Browse Source

Rework SsdpHttpClient

Bond_009 3 years ago
parent
commit
cafeedcadf

+ 17 - 16
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<Device> 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)

+ 105 - 0
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<XDocument?> 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:\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+                }
+
+                return null;
+            }
+        }
+
+        public Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
+        {
+            using var request = new HttpRequestMessage(HttpMethod.Get, url);
+            return SendRequestAsync(request, cancellationToken);
+        }
+
+        public Task<XDocument?> 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);
+        }
+    }
+}

+ 0 - 141
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -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<XDocument> 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<XDocument> 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<HttpResponseMessage> 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);
-        }
-    }
-}

+ 17 - 0
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.1 {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<JellyfinDb>();
 

+ 5 - 0
MediaBrowser.Common/Net/NamedClient.cs

@@ -14,5 +14,10 @@
         /// Gets the value for the MusicBrainz named http client.
         /// </summary>
         public const string MusicBrainz = nameof(MusicBrainz);
+
+        /// <summary>
+        /// Gets the value for the DLNA named http client.
+        /// </summary>
+        public const string Dlna = nameof(Dlna);
     }
 }