diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs index 91fb5c6a..8e158bef 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs @@ -130,6 +130,25 @@ internal static class CoreConfigTestFactory }; } + public static ProfileItem CreateHttpNode(ECoreType coreType, string indexId = "node-http-1", + string remarks = "demo-http") + { + return new ProfileItem + { + IndexId = indexId, + ConfigType = EConfigType.HTTP, + CoreType = coreType, + Remarks = remarks, + Address = "proxy.example.com", + Port = 8080, + Password = "pass", + Username = "user", + Network = nameof(ETransport.raw), + StreamSecurity = string.Empty, + Subid = string.Empty, + }; + } + public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks, IEnumerable childIndexIds) { diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs index 4847050d..576f0b1e 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/V2ray/CoreConfigV2rayServiceTests.cs @@ -28,6 +28,39 @@ public class CoreConfigV2rayServiceTests v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); } + [Fact] + public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings() + { + var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray); + CoreConfigTestFactory.BindAppManagerConfig(config); + var node = CoreConfigTestFactory.CreateHttpNode(ECoreType.Xray); + node.SetProtocolExtra(node.GetProtocolExtra() with + { + HttpHeaders = "{\"User-Agent\":\"v2rayN\",\"Set-Cookie\":[\"a=1\",\"b=2\"]}", + }); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray); + + var result = new CoreConfigV2rayService(context).GenerateClientConfigContent(); + + result.Success.Should().BeTrue(); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http"); + + outbound.settings.address?.ToString().Should().Be("proxy.example.com"); + outbound.settings.port.Should().Be(8080); + outbound.settings.user.Should().Be("user"); + outbound.settings.pass.Should().Be("pass"); + outbound.settings.level.Should().Be(1); + outbound.settings.headers.Should().NotBeNull(); + var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString()); + headers["User-Agent"]!.GetValue().Should().Be("v2rayN"); + headers["Set-Cookie"]!.AsArray() + .Select(item => item!.GetValue()) + .Should().Equal("a=1", "b=2"); + outbound.settings.servers.Should().BeNull(); + outbound.settings.vnext.Should().BeNull(); + } + [Fact] public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() { diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index d5f189e8..3c8541eb 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -146,6 +146,16 @@ public class Outboundsettings4Ray public int? port { get; set; } + public string? user { get; set; } + + public string? pass { get; set; } + + public int? level { get; set; } + + public string? email { get; set; } + + public object? headers { get; set; } + public List? peers { get; set; } public bool? noKernelTun { get; set; } diff --git a/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs b/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs index a0144ac7..56bc958a 100644 --- a/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/ProtocolExtraItem.cs @@ -5,6 +5,9 @@ public record ProtocolExtraItem public bool? Uot { get; init; } public string? CongestionControl { get; init; } + // http outbound + public string? HttpHeaders { get; init; } + // vmess public string? AlterId { get; init; } public string? VmessSecurity { get; init; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index cd70be7a..c6b6ca47 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -149,7 +149,7 @@ namespace ServiceLib.Resx { return ResourceManager.GetString("Downloading", resourceCulture); } } - + /// /// 查找类似 Failed to convert configuration file 的本地化字符串。 /// @@ -330,6 +330,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。 + /// + public static string InvalidHttpOutboundHeaders { + get { + return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture); + } + } + /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -3222,6 +3231,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 HTTP headers 的本地化字符串。 + /// + public static string TbHttpOutboundHeaders { + get { + return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture); + } + } + /// /// 查找类似 Port hopping interval 的本地化字符串。 /// @@ -4986,6 +5004,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Custom HTTP outbound request headers as a JSON object with string or string array values. 的本地化字符串。 + /// + public static string TipHttpOutboundHeaders { + get { + return ResourceManager.GetString("TipHttpOutboundHeaders", resourceCulture); + } + } + /// /// 查找类似 *Default value raw 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 67339c2b..67a6e43c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1044,6 +1044,12 @@ Congestion control + + HTTP headers + + + Custom HTTP outbound request headers as a JSON object with string or string array values. + Previous proxy remarks @@ -1806,6 +1812,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Invalid Realm URL. + + Please enter valid HTTP request headers JSON. + Format: realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port] @@ -1821,4 +1830,4 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation. - \ No newline at end of file + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index e0a84d3c..5f845da4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1041,6 +1041,12 @@ 拥塞控制算法 + + HTTP 请求头 + + + 自定义 HTTP 出站请求头,请输入值为字符串或字符串数组的 JSON 对象。 + 前置代理配置别名 @@ -1803,6 +1809,9 @@ Realm URL 不正确。 + + 请填写合法的HTTP请求头JSON。 + 格式:realm://<token>@<rendezvous-host>[:port]/<realm-name>?stun=<stun-host>[:port] @@ -1818,4 +1827,4 @@ 仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。 - \ No newline at end of file + diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 3d3d582b..88ecc735 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1041,6 +1041,12 @@ 擁塞控制算法 + + HTTP 請求頭 + + + 自訂 HTTP 出站請求頭,請輸入值為字串或字串陣列的 JSON 物件。 + 前置代理節點別名 @@ -1797,4 +1803,7 @@ 啟用則使用 sing-box TUN ,否則使用 xray TUN - \ No newline at end of file + + 請填寫合法的HTTP請求頭JSON。 + + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index c5251f9c..acd87e50 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -136,7 +136,6 @@ public partial class CoreConfigV2rayService break; } case EConfigType.SOCKS: - case EConfigType.HTTP: { ServersItem4Ray serversItem; if (outbound.settings.servers.Count <= 0) @@ -171,6 +170,31 @@ public partial class CoreConfigV2rayService outbound.settings.vnext = null; break; } + case EConfigType.HTTP: + { + outbound.settings.address = _node.Address; + outbound.settings.port = _node.Port; + + if(protocolExtra.HttpHeaders.IsNotEmpty()) + { + outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders); + } + + if (_node.Username.IsNotEmpty() + && _node.Password.IsNotEmpty()) + { + outbound.settings.user = _node.Username; + outbound.settings.pass = _node.Password; + outbound.settings.level = 1; + outbound.settings.email = Global.UserEMail; + } + + FillOutboundMux(outbound); + + outbound.settings.vnext = null; + outbound.settings.servers = null; + break; + } case EConfigType.VLESS: { VnextItem4Ray vnextItem; diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index b7ef6c26..22be8bc2 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -80,6 +80,9 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public bool NaiveQuic { get; set; } + [Reactive] + public string HttpHeadersJson { get; set; } + [Reactive] public string Hy2RealmUrl { get; set; } @@ -255,7 +258,6 @@ public class AddServerViewModel : MyReactiveObject { await SaveServerAsync(); }); - this.WhenAnyValue(x => x.Cert) .Subscribe(_ => UpdateCertTip()); @@ -311,6 +313,7 @@ public class AddServerViewModel : MyReactiveObject CongestionControl = protocolExtra.CongestionControl ?? string.Empty; InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; NaiveQuic = protocolExtra.NaiveQuic ?? false; + HttpHeadersJson = protocolExtra.HttpHeaders ?? string.Empty; Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty; GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt(); GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt(); @@ -379,6 +382,11 @@ public class AddServerViewModel : MyReactiveObject return; } } + if (HttpHeadersJson.IsNotEmpty() && JsonUtils.ParseJson(HttpHeadersJson) == null) + { + NoticeManager.Instance.Enqueue(ResUI.InvalidHttpOutboundHeaders); + return; + } SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse(CoreType); SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse; SelectedSource.MuxEnabled = MuxEnabled; @@ -416,6 +424,7 @@ public class AddServerViewModel : MyReactiveObject VmessSecurity = VmessSecurity.NullIfEmpty(), VlessEncryption = VlessEncryption.NullIfEmpty(), SsMethod = SsMethod.NullIfEmpty(), + HttpHeaders = SelectedSource.ConfigType == EConfigType.HTTP ? HttpHeadersJson.NullIfEmpty() : null, WgPublicKey = WgPublicKey.NullIfEmpty(), WgPresharedKey = WgPresharedKey.NullIfEmpty(), WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 8d92036f..675626a7 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -256,6 +256,27 @@ Grid.Column="1" Width="400" Margin="{StaticResource Margin4}" /> + + + break; case EConfigType.SOCKS: + gridSocks.IsVisible = true; + break; + case EConfigType.HTTP: gridSocks.IsVisible = true; + tbHttpHeaders.IsVisible = true; + txtHttpHeadersJson.IsVisible = true; break; case EConfigType.VLESS: @@ -148,9 +154,14 @@ public partial class AddServerWindow : WindowBase break; case EConfigType.SOCKS: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); + break; + case EConfigType.HTTP: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.HttpHeadersJson, v => v.txtHttpHeadersJson.Text).DisposeWith(disposables); break; case EConfigType.VLESS: diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml index 6d506d8c..cd39e7bd 100644 --- a/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -349,6 +349,31 @@ Width="400" Margin="{StaticResource Margin4}" Style="{StaticResource DefTextBox}" /> + + + vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); + break; + case EConfigType.HTTP: this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.HttpHeadersJson, v => v.txtHttpHeadersJson.Text).DisposeWith(disposables); break; case EConfigType.VLESS: