From b10c16a94ab6e17fe0300fbc1d2491b5c8e4d3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=94=E5=90=9B?= Date: Wed, 9 Sep 2026 14:24:05 +0800 Subject: [PATCH] Add custom HTTP headers for subscription updates (#10118) * Add custom HTTP headers for subscription updates * Res --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> --- .../Helper/HttpRequestHeadersHelperTests.cs | 77 +++++++++ .../Services/DownloadServiceHeadersTests.cs | 159 ++++++++++++++++++ v2rayN/ServiceLib/Handler/ConfigHandler.cs | 1 + .../ServiceLib/Handler/SubscriptionHandler.cs | 15 +- v2rayN/ServiceLib/Helper/DownloaderHelper.cs | 6 +- .../Helper/HttpRequestHeadersHelper.cs | 88 ++++++++++ v2rayN/ServiceLib/Models/Entities/SubItem.cs | 2 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 27 +++ v2rayN/ServiceLib/Resx/ResUI.resx | 11 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 11 +- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 12 ++ v2rayN/ServiceLib/Services/DownloadService.cs | 6 +- .../ServiceLib/ViewModels/SubEditViewModel.cs | 6 + .../v2rayN.Desktop/Views/SubEditWindow.axaml | 48 ++++-- .../Views/SubEditWindow.axaml.cs | 1 + v2rayN/v2rayN/Views/SubEditWindow.xaml | 49 ++++-- v2rayN/v2rayN/Views/SubEditWindow.xaml.cs | 1 + 17 files changed, 485 insertions(+), 35 deletions(-) create mode 100644 v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs create mode 100644 v2rayN/ServiceLib.Tests/Services/DownloadServiceHeadersTests.cs create mode 100644 v2rayN/ServiceLib/Helper/HttpRequestHeadersHelper.cs diff --git a/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs b/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs new file mode 100644 index 00000000..468f99a3 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Helper/HttpRequestHeadersHelperTests.cs @@ -0,0 +1,77 @@ +namespace ServiceLib.Tests.Helper; + +public class HttpRequestHeadersHelperTests +{ + [Test] + public async Task TryParse_ShouldAcceptEmptySettingsForExistingSubscriptions() + { + foreach (var json in new string?[] { null, "", " \r\n ", "{}" }) + { + await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue(); + await headers.Count.Should().BeEqualTo(0); + } + } + + [Test] + public async Task TryParse_ShouldPreserveValuesAndUseCaseInsensitiveNames() + { + const string json = """ + { + "X-hwid": "my_test_device", + "Authorization": "Bearer test:token", + "accept": "application/json", + "Content-Type": "application/json", + "X-Empty": "" + } + """; + + await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue(); + await headers["x-HWID"].Should().BeEqualTo("my_test_device"); + await headers["AUTHORIZATION"].Should().BeEqualTo("Bearer test:token"); + await headers["Accept"].Should().BeEqualTo("application/json"); + await headers["Content-Type"].Should().BeEqualTo("application/json"); + await headers["X-Empty"].Should().BeEqualTo(""); + } + + [Test] + [Arguments("not-json")] + [Arguments("null")] + [Arguments("[]")] + [Arguments("{\"X-Test\": 1}")] + [Arguments("{\"X-Test\": null}")] + [Arguments("{\"X-Test\": [\"one\", \"two\"]}")] + [Arguments("{\"X-Test\": \"one\", \"X-Test\": \"two\"}")] + [Arguments("{\"Accept\": \"one\", \"accept\": \"two\"}")] + [Arguments("{\"Bad Header\": \"value\"}")] + [Arguments("{\"Bad:Header\": \"value\"}")] + [Arguments("{\"\": \"value\"}")] + [Arguments("{\"X-Test\": \"one\\r\\nInjected: two\"}")] + [Arguments("{\"X-Test\": \"one\\nInjected: two\"}")] + [Arguments("{\"X-Test\": \"one\\u0000two\"}")] + public async Task TryParse_ShouldRejectInvalidHeadersWithoutReturningPartialSettings(string json) + { + await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeFalse(); + await headers.Count.Should().BeEqualTo(0); + } + + [Test] + public async Task RequestHeaders_ShouldSurviveDatabaseMigrationAndEditing() + { + using var database = new SQLiteConnection(":memory:", false); + database.Execute("CREATE TABLE SubItem (Id TEXT PRIMARY KEY, Remarks TEXT, Url TEXT)"); + database.Execute("INSERT INTO SubItem (Id, Remarks, Url) VALUES (?, ?, ?)", "existing", "Existing", "https://example.com/sub"); + database.CreateTable(); + + var item = database.Find("existing"); + await HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var oldHeaders).Should().BeTrue(); + await oldHeaders.Count.Should().BeEqualTo(0); + + item.RequestHeaders = "{\"X-hwid\":\"my_device\"}"; + database.Update(item); + await database.Find(item.Id).RequestHeaders.Should().BeEqualTo(item.RequestHeaders); + + item.RequestHeaders = ""; + database.Update(item); + await database.Find(item.Id).RequestHeaders.Should().BeEqualTo(""); + } +} diff --git a/v2rayN/ServiceLib.Tests/Services/DownloadServiceHeadersTests.cs b/v2rayN/ServiceLib.Tests/Services/DownloadServiceHeadersTests.cs new file mode 100644 index 00000000..5866b32a --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Services/DownloadServiceHeadersTests.cs @@ -0,0 +1,159 @@ +namespace ServiceLib.Tests.Services; + +public class DownloadServiceHeadersTests +{ + [Test] + [Arguments(false, false)] + [Arguments(false, true)] + [Arguments(true, false)] + [Arguments(true, true)] + public async Task TryDownloadString_ShouldSendCustomHeadersThroughBothDownloaders(bool useProxy, bool failFirstRequest) + { + await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() }); + await using var server = new SubscriptionHttpServer(failFirstRequest); + const string json = """ + { + "accept": "application/json", + "user-agent": "CustomSubscriptionClient/1.0", + "authorization": "Bearer test-token", + "X-hwid": "test-device", + "Cookie": "session=test", + "Content-Type": "application/json" + } + """; + await HttpRequestHeadersHelper.TryParse(json, out var headers).Should().BeTrue(); + var service = new DownloadService { AcceptHeader = "*/*", RequestHeaders = headers }; + var uri = new UriBuilder(useProxy ? "http://subscription.invalid/sub" : server.Url) + { + UserName = "user", + Password = "password" + }.Uri; + IWebProxy? proxy = useProxy ? new WebProxy(server.Url) : null; + + var content = await service.TryDownloadString(uri.AbsoluteUri, proxy, "OriginalClient/1.0").WaitAsync(TimeSpan.FromSeconds(20)); + + await content.Should().BeEqualTo(SubscriptionHttpServer.Body); + await (server.Requests.Count >= (failFirstRequest ? 2 : 1)).Should().BeTrue(); + foreach (var request in server.Requests) + { + await request["Accept"].Should().BeEqualTo("application/json"); + await request["User-Agent"].Should().BeEqualTo("CustomSubscriptionClient/1.0"); + await request["Authorization"].Should().BeEqualTo("Bearer test-token"); + await request["X-hwid"].Should().BeEqualTo("test-device"); + await request["Cookie"].Should().BeEqualTo("session=test"); + await request["Content-Type"].Should().BeEqualTo("application/json"); + } + } + + [Test] + [Arguments(false)] + [Arguments(true)] + public async Task TryDownloadString_ShouldKeepDefaultAcceptUserAgentAndBasicAuth(bool failFirstRequest) + { + await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() }); + await using var server = new SubscriptionHttpServer(failFirstRequest); + var service = new DownloadService { AcceptHeader = "*/*" }; + var uri = new UriBuilder(server.Url) { UserName = "user", Password = "password" }.Uri; + + var content = await service.TryDownloadString(uri.AbsoluteUri, (IWebProxy?)null, "ExistingClient/1.0").WaitAsync(TimeSpan.FromSeconds(20)); + + await content.Should().BeEqualTo(SubscriptionHttpServer.Body); + foreach (var request in server.Requests) + { + await request["Accept"].Should().BeEqualTo("*/*"); + await request["User-Agent"].Should().BeEqualTo("ExistingClient/1.0"); + await request["Authorization"].Should().BeEqualTo("Basic dXNlcjpwYXNzd29yZA=="); + await request.ContainsKey("X-hwid").Should().BeFalse(); + } + } + + [Test] + public async Task TryDownloadString_ShouldNotShareHeadersWithOtherDownloads() + { + await CertPemManager.Instance.Init(new Config { GuiItem = new GUIItem() }); + await using var server = new SubscriptionHttpServer(); + var subscription = new DownloadService + { + AcceptHeader = "*/*", + RequestHeaders = new Dictionary { ["X-hwid"] = "first-device" } + }; + var ordinaryDownload = new DownloadService(); + + await (await subscription.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body); + await (await ordinaryDownload.TryDownloadString(server.Url, (IWebProxy?)null, "TestClient/1.0")).Should().BeEqualTo(SubscriptionHttpServer.Body); + + var requests = server.Requests.ToArray(); + await requests.Length.Should().BeEqualTo(2); + await requests[0]["X-hwid"].Should().BeEqualTo("first-device"); + await requests[1].ContainsKey("X-hwid").Should().BeFalse(); + await requests[1].ContainsKey("Accept").Should().BeFalse(); + } + + private sealed class SubscriptionHttpServer : IAsyncDisposable + { + public const string Body = "subscription-test-content"; + private readonly TcpListener _listener = new(IPAddress.Loopback, 0); + private readonly CancellationTokenSource _cancellation = new(); + private readonly Task _serverTask; + private readonly bool _failFirstRequest; + + public string Url { get; } + public ConcurrentQueue> Requests { get; } = new(); + + public SubscriptionHttpServer(bool failFirstRequest = false) + { + _failFirstRequest = failFirstRequest; + _listener.Start(); + Url = $"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/subscription"; + _serverTask = ServeAsync(); + } + + private async Task ServeAsync() + { + var cancellationToken = _cancellation.Token; + while (!cancellationToken.IsCancellationRequested) + { + using var client = await _listener.AcceptTcpClientAsync(cancellationToken); + await using var stream = client.GetStream(); + using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true); + var requestLine = await reader.ReadLineAsync(cancellationToken); + if (requestLine == null) + { + continue; + } + + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + while (await reader.ReadLineAsync(cancellationToken) is { Length: > 0 } line) + { + var separator = line.IndexOf(':'); + var name = line.Substring(0, separator); + var value = line.Substring(separator + 1).Trim(); + headers[name] = headers.TryGetValue(name, out var previous) ? $"{previous}, {value}" : value; + } + Requests.Enqueue(headers); + + var status = _failFirstRequest && Requests.Count == 1 ? "503 Service Unavailable" : "200 OK"; + var body = requestLine.StartsWith("HEAD ") ? "" : Body; + var response = $"HTTP/1.1 {status}\r\nContent-Length: {Body.Length}\r\nConnection: close\r\n\r\n{body}"; + await stream.WriteAsync(Encoding.ASCII.GetBytes(response), cancellationToken); + } + } + + public async ValueTask DisposeAsync() + { + await _cancellation.CancelAsync(); + _listener.Stop(); + try + { + await _serverTask; + } + catch (OperationCanceledException) + { + } + finally + { + _cancellation.Dispose(); + } + } + } +} diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index ae775507..94b87f10 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -2202,6 +2202,7 @@ public static class ConfigHandler item.Enabled = subItem.Enabled; item.AutoUpdateInterval = subItem.AutoUpdateInterval; item.UserAgent = subItem.UserAgent; + item.RequestHeaders = subItem.RequestHeaders; item.Sort = subItem.Sort; item.Filter = subItem.Filter; item.UpdateTime = subItem.UpdateTime; diff --git a/v2rayN/ServiceLib/Handler/SubscriptionHandler.cs b/v2rayN/ServiceLib/Handler/SubscriptionHandler.cs index 30cd2c5c..fa7e9002 100644 --- a/v2rayN/ServiceLib/Handler/SubscriptionHandler.cs +++ b/v2rayN/ServiceLib/Handler/SubscriptionHandler.cs @@ -31,7 +31,7 @@ public static class SubscriptionHandler } // Create download handler - var downloadHandle = CreateDownloadHandler(hashCode, updateFunc); + var downloadHandle = CreateDownloadHandler(item, hashCode, updateFunc); await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}"); // Get all subscription content (main subscription + additional subscriptions) @@ -80,9 +80,18 @@ public static class SubscriptionHandler return true; } - private static DownloadService CreateDownloadHandler(string hashCode, Func updateFunc) + private static DownloadService CreateDownloadHandler(SubItem item, string hashCode, Func updateFunc) { - var downloadHandle = new DownloadService { AcceptHeader = "*/*" }; + if (!HttpRequestHeadersHelper.TryParse(item.RequestHeaders, out var requestHeaders)) + { + throw new FormatException(ResUI.SubRequestHeadersInvalid); + } + + var downloadHandle = new DownloadService + { + AcceptHeader = "*/*", + RequestHeaders = requestHeaders + }; downloadHandle.Error += (sender2, args) => { updateFunc?.Invoke(false, $"{hashCode}{args.GetException().Message}"); diff --git a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs index 19daefa2..e7d553ef 100644 --- a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs +++ b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs @@ -8,7 +8,8 @@ public class DownloaderHelper private static readonly Lazy _instance = new(() => new()); public static DownloaderHelper Instance => _instance.Value; - public async Task DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout) + public async Task DownloadStringAsync(IWebProxy? webProxy, string url, string? userAgent, int timeout, + IReadOnlyDictionary? requestHeaders = null, string? acceptHeader = null) { if (url.IsNullOrEmpty()) { @@ -28,6 +29,7 @@ public class DownloaderHelper var requestConfiguration = new RequestConfiguration() { Headers = headers, + Accept = acceptHeader, UserAgent = userAgent, ConnectTimeout = connectTimeout * 1000, Proxy = webProxy @@ -37,7 +39,7 @@ public class DownloaderHelper BlockTimeout = timeout * 1000, MaxTryAgainOnFailure = 2, RequestConfiguration = requestConfiguration, - CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration), + CustomHttpMessageHandlerFactory = () => HttpRequestHeadersHelper.CreateHandler(GetSocketsHttpHandler(requestConfiguration), requestHeaders), }; await using var downloader = new Downloader.DownloadService(downloadOpt); diff --git a/v2rayN/ServiceLib/Helper/HttpRequestHeadersHelper.cs b/v2rayN/ServiceLib/Helper/HttpRequestHeadersHelper.cs new file mode 100644 index 00000000..871d08a6 --- /dev/null +++ b/v2rayN/ServiceLib/Helper/HttpRequestHeadersHelper.cs @@ -0,0 +1,88 @@ +namespace ServiceLib.Helper; + +public static class HttpRequestHeadersHelper +{ + public static bool TryParse(string? json, out Dictionary headers) + { + headers = new(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(json)) + { + return true; + } + + try + { + using var document = JsonDocument.Parse(json); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + var parsed = new Dictionary(StringComparer.OrdinalIgnoreCase); + using var request = new HttpRequestMessage { Content = new ByteArrayContent([]) }; + foreach (var property in document.RootElement.EnumerateObject()) + { + if (property.Value.ValueKind != JsonValueKind.String + || !parsed.TryAdd(property.Name, property.Value.GetString()!) + || !TryAddHeader(request, property.Name, parsed[property.Name])) + { + return false; + } + } + + headers = parsed; + return true; + } + catch (Exception ex) when (ex is JsonException or ArgumentException or FormatException) + { + return false; + } + } + + public static HttpMessageHandler CreateHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary? headers) + { + return headers is { Count: > 0 } ? new RequestHeadersHandler(innerHandler, headers) : innerHandler; + } + + private static bool TryAddHeader(HttpRequestMessage request, string name, string value) + { + if (value.Any(c => char.IsControl(c) && c != '\t')) + { + return false; + } + + return request.Headers.TryAddWithoutValidation(name, value) + || request.Content!.Headers.TryAddWithoutValidation(name, value); + } + + private sealed class RequestHeadersHandler(HttpMessageHandler innerHandler, IReadOnlyDictionary headers) + : DelegatingHandler(innerHandler) + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using var customHeaders = new HttpRequestMessage { Content = new ByteArrayContent([]) }; + foreach (var header in headers) + { + if (!TryAddHeader(customHeaders, header.Key, header.Value)) + { + throw new FormatException(ResUI.SubRequestHeadersInvalid); + } + } + + // Apply after each downloader's defaults, replacing headers without changing their values. + foreach (var header in customHeaders.Headers.NonValidated) + { + request.Headers.Remove(header.Key); + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + foreach (var header in customHeaders.Content.Headers.NonValidated) + { + request.Content ??= new ByteArrayContent([]); + request.Content.Headers.Remove(header.Key); + request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/v2rayN/ServiceLib/Models/Entities/SubItem.cs b/v2rayN/ServiceLib/Models/Entities/SubItem.cs index f96f40dd..cbaf6dcd 100644 --- a/v2rayN/ServiceLib/Models/Entities/SubItem.cs +++ b/v2rayN/ServiceLib/Models/Entities/SubItem.cs @@ -16,6 +16,8 @@ public class SubItem public string UserAgent { get; set; } = string.Empty; + public string? RequestHeaders { get; set; } + public int Sort { get; set; } public string? Filter { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 35f20aa9..0829e0ae 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -555,6 +555,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 HTTP headers (JSON) 的本地化字符串。 + /// + public static string LvRequestHeaders { + get { + return ResourceManager.GetString("LvRequestHeaders", resourceCulture); + } + } + /// /// 查找类似 Type 的本地化字符串。 /// @@ -2634,6 +2643,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks. 的本地化字符串。 + /// + public static string SubRequestHeadersInvalid { + get { + return ResourceManager.GetString("SubRequestHeadersInvalid", resourceCulture); + } + } + + /// + /// 查找类似 Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service. 的本地化字符串。 + /// + public static string SubRequestHeadersTips { + get { + return ResourceManager.GetString("SubRequestHeadersTips", resourceCulture); + } + } + /// /// 查找类似 For group please leave blank here 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 4196c53c..2940e3d7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1887,7 +1887,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Xray Mux setting + + HTTP headers (JSON) + + + Optional. Example: {"X-hwid": "my_device"}. Values override default headers for all URLs in this group, including the subscription conversion service. + + + Invalid HTTP headers. Use a JSON object with unique header names and string values. Header names and values must not contain line breaks. + Process - \ 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 c289b260..3f5d9cc1 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1879,7 +1879,16 @@ Xray Mux 设置 + + HTTP 请求头 (JSON) + + + 可选,示例:{"X-hwid": "my_device"}。自定义值覆盖默认请求头,应用于本组所有订阅地址(包括订阅转换服务)。 + + + HTTP 请求头无效。请使用 JSON 对象,键名不能重复,值必须为字符串;请求头名称和值不能包含换行符。 + 进程 - \ 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 b2d43a18..5a33297a 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1864,4 +1864,16 @@ 自訂設定核心 + + HTTP 請求標頭 (JSON) + + + 可選,範例:{"X-hwid": "my_device"}。自訂值會覆寫預設標頭,套用於本組所有訂閱位址(包括訂閱轉換服務)。 + + + HTTP 請求標頭無效。請使用 JSON 物件,鍵名不能重複,值必須為字串;標頭名稱和值不能包含換行字元。 + + + 进程 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index 640c0c4d..8ec66f38 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -13,6 +13,8 @@ public class DownloadService public string? AcceptHeader { get; init; } + public IReadOnlyDictionary? RequestHeaders { get; init; } + private static readonly string _tag = "DownloadService"; /// @@ -246,7 +248,7 @@ public class DownloadService handler.SslOptions.RemoteCertificateValidationCallback = null; } - using var client = new HttpClient(handler) + using var client = new HttpClient(HttpRequestHeadersHelper.CreateHandler(handler, RequestHeaders)) { Timeout = Timeout.InfiniteTimeSpan }; @@ -297,7 +299,7 @@ public class DownloadService { userAgent = Utils.GetVersion(false); } - var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout); + var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout, RequestHeaders, AcceptHeader); return result; } catch (Exception ex) diff --git a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs index 821110a3..efc64858 100644 --- a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs @@ -77,6 +77,12 @@ public partial class SubEditViewModel : MyReactiveObject, ICloseable } } + if (!HttpRequestHeadersHelper.TryParse(SelectedSource.RequestHeaders, out _)) + { + NoticeManager.Instance.Enqueue(ResUI.SubRequestHeadersInvalid); + return; + } + SelectedSource.CustomCoreType = Enum.TryParse(CustomCoreType, out var coreType) ? coreType : null; SelectedSource.PrevProfile = PrevProfile; SelectedSource.NextProfile = NextProfile; diff --git a/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml b/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml index 77f7d997..b55ae847 100644 --- a/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/SubEditWindow.axaml @@ -186,79 +186,101 @@ Grid.Row="8" Grid.Column="0" Margin="{StaticResource Margin4}" + VerticalAlignment="Top" + Text="{x:Static resx:ResUI.LvRequestHeaders}" /> + + + + + +