DlnaHttpClient.cs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #pragma warning disable CS1591
  2. using System;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Net.Http;
  6. using System.Net.Mime;
  7. using System.Text;
  8. using System.Text.RegularExpressions;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using System.Xml;
  12. using System.Xml.Linq;
  13. using Emby.Dlna.Common;
  14. using MediaBrowser.Common.Net;
  15. using Microsoft.Extensions.Logging;
  16. namespace Emby.Dlna.PlayTo
  17. {
  18. /// <summary>
  19. /// Http client for Dlna PlayTo function.
  20. /// </summary>
  21. public partial class DlnaHttpClient
  22. {
  23. private readonly ILogger _logger;
  24. private readonly IHttpClientFactory _httpClientFactory;
  25. public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
  26. {
  27. _logger = logger;
  28. _httpClientFactory = httpClientFactory;
  29. }
  30. [GeneratedRegex("(&(?![a-z]*;))")]
  31. private static partial Regex EscapeAmpersandRegex();
  32. private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
  33. {
  34. // If it's already a complete url, don't stick anything onto the front of it
  35. if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
  36. {
  37. return serviceUrl;
  38. }
  39. if (!serviceUrl.StartsWith('/'))
  40. {
  41. serviceUrl = "/" + serviceUrl;
  42. }
  43. return baseUrl + serviceUrl;
  44. }
  45. private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  46. {
  47. var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
  48. using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
  49. response.EnsureSuccessStatusCode();
  50. await using MemoryStream ms = new MemoryStream();
  51. await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
  52. try
  53. {
  54. return await XDocument.LoadAsync(
  55. ms,
  56. LoadOptions.None,
  57. cancellationToken).ConfigureAwait(false);
  58. }
  59. catch (XmlException)
  60. {
  61. // try correcting the Xml response with common errors
  62. ms.Position = 0;
  63. using StreamReader sr = new StreamReader(ms);
  64. var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
  65. // find and replace unescaped ampersands (&)
  66. xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
  67. try
  68. {
  69. // retry reading Xml
  70. using var xmlReader = new StringReader(xmlString);
  71. return await XDocument.LoadAsync(
  72. xmlReader,
  73. LoadOptions.None,
  74. cancellationToken).ConfigureAwait(false);
  75. }
  76. catch (XmlException ex)
  77. {
  78. _logger.LogError(ex, "Failed to parse response");
  79. _logger.LogDebug("Malformed response: {Content}\n", xmlString);
  80. return null;
  81. }
  82. }
  83. }
  84. public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
  85. {
  86. using var request = new HttpRequestMessage(HttpMethod.Get, url);
  87. // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
  88. return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
  89. }
  90. public async Task<XDocument?> SendCommandAsync(
  91. string baseUrl,
  92. DeviceService service,
  93. string command,
  94. string postData,
  95. string? header = null,
  96. CancellationToken cancellationToken = default)
  97. {
  98. using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
  99. {
  100. Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
  101. };
  102. request.Headers.TryAddWithoutValidation(
  103. "SOAPACTION",
  104. string.Format(
  105. CultureInfo.InvariantCulture,
  106. "\"{0}#{1}\"",
  107. service.ServiceType,
  108. command));
  109. request.Headers.Pragma.ParseAdd("no-cache");
  110. if (!string.IsNullOrEmpty(header))
  111. {
  112. request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
  113. }
  114. // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
  115. return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
  116. }
  117. }
  118. }