diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 8eb90f4455..0b480f5ab0 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -235,7 +235,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -276,7 +276,7 @@ namespace Emby.Dlna.PlayTo // Remote control will perform better Volume = value; - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -303,7 +303,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -343,7 +343,7 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -400,7 +400,8 @@ namespace Emby.Dlna.PlayTo } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); - await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken) + await new DlnaHttpClient(_logger, _httpClientFactory) + .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken) .ConfigureAwait(false); } @@ -428,7 +429,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - return new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -461,7 +462,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -485,7 +486,7 @@ namespace Emby.Dlna.PlayTo var service = GetAvTransportService(); - await new SsdpHttpClient(_httpClientFactory) + await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, @@ -618,7 +619,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -668,7 +669,7 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -701,7 +702,7 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -747,7 +748,7 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -819,7 +820,7 @@ namespace Emby.Dlna.PlayTo return (false, null); } - var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( + var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, @@ -997,7 +998,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClientFactory); + var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); if (document == null) @@ -1029,7 +1030,7 @@ namespace Emby.Dlna.PlayTo string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - var httpClient = new SsdpHttpClient(_httpClientFactory); + var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); if (document == null) @@ -1064,7 +1065,7 @@ namespace Emby.Dlna.PlayTo public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { - var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); + var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); if (document == null) diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs new file mode 100644 index 0000000000..62156d1833 --- /dev/null +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -0,0 +1,105 @@ +#pragma warning disable CS1591 + +using System; +using System.Globalization; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Emby.Dlna.Common; +using MediaBrowser.Common.Net; +using Microsoft.Extensions.Logging; + +namespace Emby.Dlna.PlayTo +{ + public class DlnaHttpClient + { + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) + { + // If it's already a complete url, don't stick anything onto the front of it + if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return serviceUrl; + } + + if (!serviceUrl.StartsWith('/')) + { + serviceUrl = "/" + serviceUrl; + } + + return baseUrl + serviceUrl; + } + + private async Task SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + try + { + return await XDocument.LoadAsync( + stream, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); + } + catch (XmlException ex) + { + _logger.LogError(ex, "Failed to parse response"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); + } + + return null; + } + } + + public Task GetDataAsync(string url, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + return SendRequestAsync(request, cancellationToken); + } + + public Task SendCommandAsync( + string baseUrl, + DeviceService service, + string command, + string postData, + string? header = null, + CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl)) + { + Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml) + }; + + request.Headers.TryAddWithoutValidation( + "SOAPACTION", + string.Format( + CultureInfo.InvariantCulture, + "\"{0}#{1}\"", + service.ServiceType, + command)); + request.Headers.Pragma.ParseAdd("no-cache"); + + if (!string.IsNullOrEmpty(header)) + { + request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); + } + + return SendRequestAsync(request, cancellationToken); + } + } +} diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs deleted file mode 100644 index cade7b4c2c..0000000000 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ /dev/null @@ -1,141 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Net.Http; -using System.Net.Mime; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml.Linq; -using Emby.Dlna.Common; -using MediaBrowser.Common.Net; - -namespace Emby.Dlna.PlayTo -{ - public class SsdpHttpClient - { - private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; - private const string FriendlyName = "Jellyfin"; - - private readonly IHttpClientFactory _httpClientFactory; - - public SsdpHttpClient(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - public async Task SendCommandAsync( - string baseUrl, - DeviceService service, - string command, - string postData, - string header = null, - CancellationToken cancellationToken = default) - { - var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); - using var response = await PostSoapDataAsync( - url, - $"\"{service.ServiceType}#{command}\"", - postData, - header, - cancellationToken) - .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await XDocument.LoadAsync( - stream, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - - private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) - { - // If it's already a complete url, don't stick anything onto the front of it - if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return serviceUrl; - } - - if (!serviceUrl.StartsWith('/')) - { - serviceUrl = "/" + serviceUrl; - } - - return baseUrl + serviceUrl; - } - - public async Task SubscribeAsync( - string url, - string ip, - int port, - string localIp, - int eventport, - int timeOut = 3600) - { - using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url); - options.Headers.UserAgent.ParseAdd(USERAGENT); - options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture)); - options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">"); - options.Headers.TryAddWithoutValidation("NT", "upnp:event"); - options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture)); - - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(options, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - } - - public async Task GetDataAsync(string url, CancellationToken cancellationToken) - { - using var options = new HttpRequestMessage(HttpMethod.Get, url); - options.Headers.UserAgent.ParseAdd(USERAGENT); - options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - try - { - return await XDocument.LoadAsync( - stream, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch - { - return null; - } - } - - private async Task PostSoapDataAsync( - string url, - string soapAction, - string postData, - string header, - CancellationToken cancellationToken) - { - if (soapAction[0] != '\"') - { - soapAction = $"\"{soapAction}\""; - } - - using var options = new HttpRequestMessage(HttpMethod.Post, url); - options.Headers.UserAgent.ParseAdd(USERAGENT); - options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction); - options.Headers.TryAddWithoutValidation("Pragma", "no-cache"); - options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); - - if (!string.IsNullOrEmpty(header)) - { - options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); - } - - options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml); - - return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 8eb5f21960..1954a5c558 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -103,6 +104,22 @@ namespace Jellyfin.Server }) .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); + services.AddHttpClient(NamedClient.Dlna, c => + { + c.DefaultRequestHeaders.UserAgent.ParseAdd( + string.Format( + CultureInfo.InvariantCulture, + "{0}/{1} UPnP/1.0 {2}/{3}", + MediaBrowser.Common.System.OperatingSystem.Name, + Environment.OSVersion, + _serverApplicationHost.Name, + _serverApplicationHost.ApplicationVersionString)); + + c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0 + c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from? + }) + .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); + services.AddHealthChecks() .AddDbContextCheck(); diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs index 0f6161c328..a6cacd4f17 100644 --- a/MediaBrowser.Common/Net/NamedClient.cs +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -14,5 +14,10 @@ /// 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); } }