#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Mime; using System.Text; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; namespace Emby.Dlna.Eventing { public class DlnaEventManager : IDlnaEventManager { private readonly ConcurrentDictionary _subscriptions = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; _logger = logger; } public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl) { var subscription = GetSubscription(subscriptionId, false); if (subscription != null) { subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300; int timeoutSeconds = subscription.TimeoutSeconds; subscription.SubscriptionTime = DateTime.UtcNow; _logger.LogDebug( "Renewing event subscription for {0} with timeout of {1} to {2}", subscription.NotificationType, timeoutSeconds, subscription.CallbackUrl); return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds); } return new EventSubscriptionResponse(string.Empty, "text/plain"); } public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) { var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); _logger.LogDebug( "Creating event subscription for {0} with timeout of {1} to {2}", notificationType, timeout, callbackUrl); _subscriptions.TryAdd(id, new EventSubscription { Id = id, CallbackUrl = callbackUrl, SubscriptionTime = DateTime.UtcNow, TimeoutSeconds = timeout, NotificationType = notificationType }); return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); } private int? ParseTimeout(string header) { if (!string.IsNullOrEmpty(header)) { // Starts with SECOND- header = header.Split('-')[^1]; if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val)) { return val; } } return null; } public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) { _logger.LogDebug("Cancelling event subscription {0}", subscriptionId); _subscriptions.TryRemove(subscriptionId, out _); return new EventSubscriptionResponse(string.Empty, "text/plain"); } private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) { var response = new EventSubscriptionResponse(string.Empty, "text/plain"); response.Headers["SID"] = subscriptionId; response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString; return response; } public EventSubscription GetSubscription(string id) { return GetSubscription(id, false); } private EventSubscription GetSubscription(string id, bool throwOnMissing) { if (!_subscriptions.TryGetValue(id, out EventSubscription e) && throwOnMissing) { throw new ResourceNotFoundException("Event with Id " + id + " not found."); } return e; } public Task TriggerEvent(string notificationType, IDictionary stateVariables) { var subs = _subscriptions.Values .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase)) .ToList(); var tasks = subs.Select(i => TriggerEvent(i, stateVariables)); return Task.WhenAll(tasks); } private async Task TriggerEvent(EventSubscription subscription, IDictionary stateVariables) { var builder = new StringBuilder(); builder.Append(""); builder.Append(""); foreach (var key in stateVariables.Keys) { builder.Append("") .Append('<') .Append(key) .Append('>') .Append(stateVariables[key]) .Append("') .Append(""); } builder.Append(""); using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); options.Headers.TryAddWithoutValidation("SID", subscription.Id); options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture)); try { using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } catch (OperationCanceledException) { } catch { // Already logged at lower levels } finally { subscription.IncrementTriggerCount(); } } } }