DlnaHttpClient.cs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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. Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
  51. await using (stream.ConfigureAwait(false))
  52. {
  53. try
  54. {
  55. return await XDocument.LoadAsync(
  56. stream,
  57. LoadOptions.None,
  58. cancellationToken).ConfigureAwait(false);
  59. }
  60. catch (XmlException)
  61. {
  62. // try correcting the Xml response with common errors
  63. stream.Position = 0;
  64. using StreamReader sr = new StreamReader(stream);
  65. var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
  66. // find and replace unescaped ampersands (&)
  67. xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
  68. try
  69. {
  70. // retry reading Xml
  71. using var xmlReader = new StringReader(xmlString);
  72. return await XDocument.LoadAsync(
  73. xmlReader,
  74. LoadOptions.None,
  75. cancellationToken).ConfigureAwait(false);
  76. }
  77. catch (XmlException ex)
  78. {
  79. _logger.LogError(ex, "Failed to parse response");
  80. _logger.LogDebug("Malformed response: {Content}\n", xmlString);
  81. return null;
  82. }
  83. }
  84. }
  85. }
  86. public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
  87. {
  88. using var request = new HttpRequestMessage(HttpMethod.Get, url);
  89. // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
  90. return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
  91. }
  92. public async Task<XDocument?> SendCommandAsync(
  93. string baseUrl,
  94. DeviceService service,
  95. string command,
  96. string postData,
  97. string? header = null,
  98. CancellationToken cancellationToken = default)
  99. {
  100. using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
  101. {
  102. Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
  103. };
  104. request.Headers.TryAddWithoutValidation(
  105. "SOAPACTION",
  106. string.Format(
  107. CultureInfo.InvariantCulture,
  108. "\"{0}#{1}\"",
  109. service.ServiceType,
  110. command));
  111. request.Headers.Pragma.ParseAdd("no-cache");
  112. if (!string.IsNullOrEmpty(header))
  113. {
  114. request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
  115. }
  116. // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
  117. return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
  118. }
  119. }
  120. }