From 0b62471828ae5723c49aa6efc66c83d6a06dbcb7 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 26 Feb 2024 11:09:02 -0700 Subject: [PATCH 1/2] Add helpers to register and get a plugin HttpClient --- .../ApplicationHost.cs | 117 ++++++++++++++++++ Jellyfin.Server/CoreAppHost.cs | 1 - Jellyfin.Server/Startup.cs | 53 +------- .../Extensions/HttpClientFactoryExtensions.cs | 22 ++++ MediaBrowser.Common/Net/NamedClient.cs | 35 ++---- .../IServerApplicationHost.cs | 15 +++ 6 files changed, 168 insertions(+), 75 deletions(-) create mode 100644 MediaBrowser.Common/Extensions/HttpClientFactoryExtensions.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 745753440d..df6b8ecc3f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -10,8 +10,12 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; @@ -37,6 +41,7 @@ using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; +using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; using Jellyfin.Server.Implementations; @@ -281,6 +286,118 @@ namespace Emby.Server.Implementations .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); } + /// + public void AddPluginHttpClient( + IServiceCollection serviceCollection, + string basedOnClientName, + (string Name, string[] Values)[] customHeaders = null) + where T : BasePlugin + { + ArgumentNullException.ThrowIfNull(serviceCollection); + ArgumentException.ThrowIfNullOrEmpty(basedOnClientName); + + var pluginType = typeof(T); + + if (string.Equals(NamedClient.Default, basedOnClientName, StringComparison.Ordinal) + || string.Equals(NamedClient.DirectIp, basedOnClientName, StringComparison.Ordinal)) + { + AddHttpClient(serviceCollection, basedOnClientName, customHeaders, pluginType); + } + else + { + throw new InvalidOperationException($"Unknown HttpClient name '{basedOnClientName}' for plugin '{pluginType.Name}'"); + } + } + + /// + /// Add HttpClient. + /// Intended for internal use only. + /// + /// The service collection. + /// The name of the HttpClient to base this client on. + /// The custom headers to use. + /// The type of plugin adding the HttpClient. + public void AddHttpClient( + IServiceCollection serviceCollection, + string basedOnClientName, + (string Name, string[] Values)[] customHeaders = null, + Type pluginType = null) + { + var clientName = basedOnClientName; + ProductInfoHeaderValue pluginHeader = null; + if (pluginType is not null) + { + var assembly = pluginType.Assembly.GetName(); + clientName = pluginType.Name; + pluginHeader = new ProductInfoHeaderValue( + clientName.Replace(' ', '-').Replace('.', '-'), + assembly.Version!.ToString(3)); + } + + if (string.Equals(NamedClient.Default, basedOnClientName, StringComparison.Ordinal)) + { + serviceCollection.AddHttpClient(clientName, ConfigureClient) + .ConfigurePrimaryHttpMessageHandler(EyeballsHttpClientHandlerDelegate); + } + else if (string.Equals(NamedClient.DirectIp, basedOnClientName, StringComparison.Ordinal)) + { + serviceCollection.AddHttpClient(clientName, ConfigureClient) + .ConfigurePrimaryHttpMessageHandler(DefaultHttpClientHandlerDelegate); + } + else + { + throw new InvalidOperationException($"Unknown HttpClient name '{basedOnClientName}'"); + } + + Logger.LogInformation("Added HttpClient '{ClientName}'", clientName); + + return; + + void ConfigureClient(HttpClient c) + { + var productHeader = new ProductInfoHeaderValue(Name.Replace(' ', '-'), ApplicationVersionString); + var contactHeader = new ProductInfoHeaderValue($"({ApplicationUserAgentAddress})"); + var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0); + var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9); + var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8); + + if (customHeaders is null) + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.UserAgent.Add(contactHeader); + if (pluginHeader is not null) + { + c.DefaultRequestHeaders.UserAgent.Add(pluginHeader); + } + + c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); + c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); + c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); + } + else + { + c.DefaultRequestHeaders.Clear(); + foreach (var (name, values) in customHeaders) + { + c.DefaultRequestHeaders.Add(name, values); + } + } + } + + static HttpMessageHandler DefaultHttpClientHandlerDelegate(IServiceProvider s) => new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 + }; + + static HttpMessageHandler EyeballsHttpClientHandlerDelegate(IServiceProvider s) => new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8, + ConnectCallback = HttpClientExtension.OnConnect + }; + } + /// /// Creates the instance safe. /// diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index d5b6e93b8e..e50efd3af7 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -26,7 +26,6 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Activity; -using MediaBrowser.Providers.Lyric; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index e9fb3e4c27..7e805f3a0d 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,16 +1,10 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Mime; -using System.Text; using Emby.Server.Implementations.EntryPoints; using Jellyfin.Api.Middleware; using Jellyfin.LiveTv.Extensions; using Jellyfin.LiveTv.Recordings; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking; -using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations; @@ -75,51 +69,8 @@ namespace Jellyfin.Server services.AddJellyfinApiAuthorization(); - var productHeader = new ProductInfoHeaderValue( - _serverApplicationHost.Name.Replace(' ', '-'), - _serverApplicationHost.ApplicationVersionString); - var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0); - var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9); - var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8); - Func eyeballsHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() - { - AutomaticDecompression = DecompressionMethods.All, - RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8, - ConnectCallback = HttpClientExtension.OnConnect - }; - - Func defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() - { - AutomaticDecompression = DecompressionMethods.All, - RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 - }; - - services.AddHttpClient(NamedClient.Default, c => - { - c.DefaultRequestHeaders.UserAgent.Add(productHeader); - c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); - c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); - c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); - }) - .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); - - services.AddHttpClient(NamedClient.MusicBrainz, c => - { - c.DefaultRequestHeaders.UserAgent.Add(productHeader); - c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); - c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); - c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); - }) - .ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate); - - services.AddHttpClient(NamedClient.DirectIp, c => - { - c.DefaultRequestHeaders.UserAgent.Add(productHeader); - c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader); - c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); - c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); - }) - .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); + _serverApplicationHost.AddHttpClient(services, NamedClient.Default); + _serverApplicationHost.AddHttpClient(services, NamedClient.DirectIp); services.AddHealthChecks() .AddCheck>(nameof(JellyfinDbContext)); diff --git a/MediaBrowser.Common/Extensions/HttpClientFactoryExtensions.cs b/MediaBrowser.Common/Extensions/HttpClientFactoryExtensions.cs new file mode 100644 index 0000000000..dd239790b6 --- /dev/null +++ b/MediaBrowser.Common/Extensions/HttpClientFactoryExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Http; +using MediaBrowser.Common.Plugins; + +namespace MediaBrowser.Common.Extensions; + +/// +/// Extensions for . +/// +public static class HttpClientFactoryExtensions +{ + /// + /// Get a plugin-configured HttpClient. + /// This requires calling AddHttpClient{T} during RegisterServices. + /// + /// The http client factory. + /// The type of plugin. + /// The HttpClient. + public static HttpClient GetPluginHttpClient(this IHttpClientFactory httpClientFactory) + where T : BasePlugin + => httpClientFactory.CreateClient(typeof(T).Name); +} diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs index 9c5544b0ff..e585a18c05 100644 --- a/MediaBrowser.Common/Net/NamedClient.cs +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -1,28 +1,17 @@ -namespace MediaBrowser.Common.Net +namespace MediaBrowser.Common.Net; + +/// +/// Registered http client names. +/// +public static class NamedClient { /// - /// Registered http client names. + /// Gets the value for the default named http client which implements happy eyeballs. /// - public static class NamedClient - { - /// - /// Gets the value for the default named http client which implements happy eyeballs. - /// - public const string Default = nameof(Default); - - /// - /// Gets the value for the MusicBrainz named http client. - /// - public const string MusicBrainz = nameof(MusicBrainz); - - /// - /// Gets the value for the DLNA named http client. - /// - public const string Dlna = nameof(Dlna); + public const string Default = nameof(Default); - /// - /// Non happy eyeballs implementation. - /// - public const string DirectIp = nameof(DirectIp); - } + /// + /// Non happy eyeballs implementation. + /// + public const string DirectIp = nameof(DirectIp); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index e9c4d9e19a..9cac639553 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,7 +4,9 @@ using System.Net; using MediaBrowser.Common; +using MediaBrowser.Common.Plugins; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Controller { @@ -86,5 +88,18 @@ namespace MediaBrowser.Controller string ExpandVirtualPath(string path); string ReverseVirtualPath(string path); + + /// + /// Add a plugin HttpClient. + /// + /// The service collection to add to. + /// The name of the HttpClient to base config on. + /// The custom headers to use. If provided no other headers will be added. + /// The plugin type. + void AddPluginHttpClient( + IServiceCollection serviceCollection, + string basedOnClientName, + (string Name, string[] Values)[] customHeaders = null) + where T : BasePlugin; } } From 07140142fb1860e0bd30a81951e32a8f5013548b Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 26 Feb 2024 17:57:42 -0700 Subject: [PATCH 2/2] Set the default basedOnClientName --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- MediaBrowser.Controller/IServerApplicationHost.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index df6b8ecc3f..76e337909b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -290,7 +290,7 @@ namespace Emby.Server.Implementations public void AddPluginHttpClient( IServiceCollection serviceCollection, string basedOnClientName, - (string Name, string[] Values)[] customHeaders = null) + (string Name, string[] Values)[] customHeaders) where T : BasePlugin { ArgumentNullException.ThrowIfNull(serviceCollection); diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 9cac639553..915ae9cbd2 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -4,6 +4,7 @@ using System.Net; using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -98,7 +99,7 @@ namespace MediaBrowser.Controller /// The plugin type. void AddPluginHttpClient( IServiceCollection serviceCollection, - string basedOnClientName, + string basedOnClientName = nameof(NamedClient.Default), (string Name, string[] Values)[] customHeaders = null) where T : BasePlugin; }