SsdpHandler.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Events;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Dlna.Server;
  5. using MediaBrowser.Model.Logging;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Globalization;
  10. using System.Linq;
  11. using System.Net;
  12. using System.Net.Sockets;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. namespace MediaBrowser.Dlna.Ssdp
  17. {
  18. public class SsdpHandler : IDisposable
  19. {
  20. private Socket _socket;
  21. private readonly ILogger _logger;
  22. private readonly IServerConfigurationManager _config;
  23. const string SSDPAddr = "239.255.255.250";
  24. const int SSDPPort = 1900;
  25. private readonly string _serverSignature;
  26. private readonly IPAddress _ssdpIp = IPAddress.Parse(SSDPAddr);
  27. private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort);
  28. private Timer _queueTimer;
  29. private Timer _notificationTimer;
  30. private readonly AutoResetEvent _datagramPosted = new AutoResetEvent(false);
  31. private readonly ConcurrentQueue<Datagram> _messageQueue = new ConcurrentQueue<Datagram>();
  32. private bool _isDisposed;
  33. private readonly ConcurrentDictionary<Guid, List<UpnpDevice>> _devices = new ConcurrentDictionary<Guid, List<UpnpDevice>>();
  34. public SsdpHandler(ILogger logger, IServerConfigurationManager config, string serverSignature)
  35. {
  36. _logger = logger;
  37. _config = config;
  38. _serverSignature = serverSignature;
  39. _config.NamedConfigurationUpdated += _config_ConfigurationUpdated;
  40. }
  41. void _config_ConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
  42. {
  43. if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
  44. {
  45. ReloadAliveNotifier();
  46. }
  47. }
  48. public event EventHandler<SsdpMessageEventArgs> MessageReceived;
  49. private async void OnMessageReceived(SsdpMessageEventArgs args)
  50. {
  51. if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase))
  52. {
  53. var headers = args.Headers;
  54. TimeSpan delay = GetSearchDelay(headers);
  55. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  56. {
  57. _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
  58. }
  59. await Task.Delay(delay).ConfigureAwait(false);
  60. string st;
  61. if (headers.TryGetValue("st", out st))
  62. {
  63. RespondToSearch(args.EndPoint, st);
  64. }
  65. }
  66. EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
  67. }
  68. public IEnumerable<UpnpDevice> RegisteredDevices
  69. {
  70. get
  71. {
  72. return _devices.Values.SelectMany(i => i).ToList();
  73. }
  74. }
  75. public void Start()
  76. {
  77. _socket = CreateMulticastSocket();
  78. _logger.Info("SSDP service started");
  79. Receive();
  80. ReloadAliveNotifier();
  81. }
  82. public void SendSearchMessage(EndPoint localIp)
  83. {
  84. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  85. values["HOST"] = "239.255.255.250:1900";
  86. values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2";
  87. values["MAN"] = "\"ssdp:discover\"";
  88. // Search target
  89. values["ST"] = "ssdp:all";
  90. // Seconds to delay response
  91. values["MX"] = "3";
  92. // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2)
  93. SendDatagram("M-SEARCH * HTTP/1.1", values, localIp, 1);
  94. }
  95. public void SendDatagram(string header,
  96. Dictionary<string, string> values,
  97. EndPoint localAddress,
  98. int sendCount)
  99. {
  100. SendDatagram(header, values, _ssdpEndp, localAddress, false, sendCount);
  101. }
  102. public void SendDatagram(string header,
  103. Dictionary<string, string> values,
  104. EndPoint endpoint,
  105. EndPoint localAddress,
  106. bool ignoreBindFailure,
  107. int sendCount)
  108. {
  109. var msg = new SsdpMessageBuilder().BuildMessage(header, values);
  110. var queued = false;
  111. for (var i = 0; i < sendCount; i++)
  112. {
  113. var dgram = new Datagram(endpoint, localAddress, _logger, msg, ignoreBindFailure);
  114. if (_messageQueue.Count == 0)
  115. {
  116. dgram.Send();
  117. }
  118. else
  119. {
  120. _messageQueue.Enqueue(dgram);
  121. queued = true;
  122. }
  123. }
  124. if (queued)
  125. {
  126. StartQueueTimer();
  127. }
  128. }
  129. /// <summary>
  130. /// According to the spec: http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf
  131. /// Device responses should be delayed a random duration between 0 and this many seconds to balance
  132. /// load for the control point when it processes responses. In my testing kodi times out after mx
  133. /// so we will generate from mx - 1
  134. /// </summary>
  135. /// <param name="headers">The mx headers</param>
  136. /// <returns>A timepsan for the amount to delay before returning search result.</returns>
  137. private TimeSpan GetSearchDelay(Dictionary<string, string> headers)
  138. {
  139. string mx;
  140. headers.TryGetValue("mx", out mx);
  141. int delaySeconds = 0;
  142. if (!string.IsNullOrWhiteSpace(mx)
  143. && int.TryParse(mx, NumberStyles.Any, CultureInfo.InvariantCulture, out delaySeconds)
  144. && delaySeconds > 1)
  145. {
  146. delaySeconds = new Random().Next(delaySeconds - 1);
  147. }
  148. return TimeSpan.FromSeconds(delaySeconds);
  149. }
  150. private void RespondToSearch(EndPoint endpoint, string deviceType)
  151. {
  152. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  153. {
  154. _logger.Debug("RespondToSearch");
  155. }
  156. const string header = "HTTP/1.1 200 OK";
  157. foreach (var d in RegisteredDevices)
  158. {
  159. if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) ||
  160. string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase))
  161. {
  162. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  163. values["CACHE-CONTROL"] = "max-age = 600";
  164. values["DATE"] = DateTime.Now.ToString("R");
  165. values["EXT"] = "";
  166. values["LOCATION"] = d.Descriptor.ToString();
  167. values["SERVER"] = _serverSignature;
  168. values["ST"] = d.Type;
  169. values["USN"] = d.USN;
  170. SendDatagram(header, values, endpoint, null, true, 1);
  171. SendDatagram(header, values, endpoint, new IPEndPoint(d.Address, 0), true, 1);
  172. //SendDatagram(header, values, endpoint, null, true);
  173. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  174. {
  175. _logger.Debug("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString());
  176. }
  177. }
  178. }
  179. }
  180. private readonly object _queueTimerSyncLock = new object();
  181. private void StartQueueTimer()
  182. {
  183. lock (_queueTimerSyncLock)
  184. {
  185. if (_queueTimer == null)
  186. {
  187. _queueTimer = new Timer(QueueTimerCallback, null, 500, Timeout.Infinite);
  188. }
  189. else
  190. {
  191. _queueTimer.Change(500, Timeout.Infinite);
  192. }
  193. }
  194. }
  195. private void QueueTimerCallback(object state)
  196. {
  197. Datagram msg;
  198. while (_messageQueue.TryDequeue(out msg))
  199. {
  200. msg.Send();
  201. }
  202. _datagramPosted.Set();
  203. if (_messageQueue.Count > 0)
  204. {
  205. StartQueueTimer();
  206. }
  207. else
  208. {
  209. DisposeQueueTimer();
  210. }
  211. }
  212. private void Receive()
  213. {
  214. try
  215. {
  216. var buffer = new byte[1024];
  217. EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort);
  218. _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer);
  219. }
  220. catch (ObjectDisposedException)
  221. {
  222. }
  223. catch (Exception ex)
  224. {
  225. _logger.Debug("Error in BeginReceiveFrom", ex);
  226. }
  227. }
  228. private void ReceiveCallback(IAsyncResult result)
  229. {
  230. if (_isDisposed)
  231. {
  232. return;
  233. }
  234. try
  235. {
  236. EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort);
  237. var length = _socket.EndReceiveFrom(result, ref endpoint);
  238. var received = (byte[])result.AsyncState;
  239. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  240. {
  241. _logger.Debug(Encoding.ASCII.GetString(received));
  242. }
  243. var args = SsdpHelper.ParseSsdpResponse(received);
  244. args.EndPoint = endpoint;
  245. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  246. {
  247. var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
  248. var headerText = string.Join(",", headerTexts.ToArray());
  249. _logger.Debug("{0} message received from {1} on {3}. Headers: {2}", args.Method, args.EndPoint, headerText, _socket.LocalEndPoint);
  250. }
  251. OnMessageReceived(args);
  252. }
  253. catch (Exception ex)
  254. {
  255. _logger.ErrorException("Failed to read SSDP message", ex);
  256. }
  257. if (_socket != null)
  258. {
  259. Receive();
  260. }
  261. }
  262. public void Dispose()
  263. {
  264. _config.NamedConfigurationUpdated -= _config_ConfigurationUpdated;
  265. _isDisposed = true;
  266. while (_messageQueue.Count != 0)
  267. {
  268. _datagramPosted.WaitOne();
  269. }
  270. DisposeSocket();
  271. DisposeQueueTimer();
  272. DisposeNotificationTimer();
  273. _datagramPosted.Dispose();
  274. }
  275. private void DisposeSocket()
  276. {
  277. if (_socket != null)
  278. {
  279. _socket.Close();
  280. _socket.Dispose();
  281. _socket = null;
  282. }
  283. }
  284. private void DisposeQueueTimer()
  285. {
  286. lock (_queueTimerSyncLock)
  287. {
  288. if (_queueTimer != null)
  289. {
  290. _queueTimer.Dispose();
  291. _queueTimer = null;
  292. }
  293. }
  294. }
  295. private Socket CreateMulticastSocket()
  296. {
  297. var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
  298. socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
  299. socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
  300. socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
  301. socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(_ssdpIp, 0));
  302. socket.Bind(new IPEndPoint(IPAddress.Any, SSDPPort));
  303. return socket;
  304. }
  305. private void NotifyAll()
  306. {
  307. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  308. {
  309. _logger.Debug("Sending alive notifications");
  310. }
  311. foreach (var d in RegisteredDevices)
  312. {
  313. NotifyDevice(d, "alive");
  314. }
  315. }
  316. private void NotifyDevice(UpnpDevice dev, string type, int sendCount = 1)
  317. {
  318. const string header = "NOTIFY * HTTP/1.1";
  319. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  320. // If needed later for non-server devices, these headers will need to be dynamic
  321. values["HOST"] = "239.255.255.250:1900";
  322. values["CACHE-CONTROL"] = "max-age = 600";
  323. values["LOCATION"] = dev.Descriptor.ToString();
  324. values["SERVER"] = _serverSignature;
  325. values["NTS"] = "ssdp:" + type;
  326. values["NT"] = dev.Type;
  327. values["USN"] = dev.USN;
  328. if (_config.GetDlnaConfiguration().EnableDebugLogging)
  329. {
  330. _logger.Debug("{0} said {1}", dev.USN, type);
  331. }
  332. SendDatagram(header, values, new IPEndPoint(dev.Address, 0), sendCount);
  333. }
  334. public void RegisterNotification(Guid uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services)
  335. {
  336. List<UpnpDevice> list;
  337. lock (_devices)
  338. {
  339. if (!_devices.TryGetValue(uuid, out list))
  340. {
  341. _devices.TryAdd(uuid, list = new List<UpnpDevice>());
  342. }
  343. }
  344. list.AddRange(services.Select(i => new UpnpDevice(uuid, i, descriptionUri, address)));
  345. NotifyAll();
  346. _logger.Debug("Registered mount {0} at {1}", uuid, descriptionUri);
  347. }
  348. public void UnregisterNotification(Guid uuid)
  349. {
  350. List<UpnpDevice> dl;
  351. if (_devices.TryRemove(uuid, out dl))
  352. {
  353. foreach (var d in dl.ToList())
  354. {
  355. NotifyDevice(d, "byebye", 2);
  356. }
  357. _logger.Debug("Unregistered mount {0}", uuid);
  358. }
  359. }
  360. private readonly object _notificationTimerSyncLock = new object();
  361. private int _aliveNotifierIntervalMs;
  362. private void ReloadAliveNotifier()
  363. {
  364. if (!_config.GetDlnaConfiguration().BlastAliveMessages)
  365. {
  366. DisposeNotificationTimer();
  367. return;
  368. }
  369. var intervalMs = _config.GetDlnaConfiguration().BlastAliveMessageIntervalSeconds * 1000;
  370. if (_notificationTimer == null || _aliveNotifierIntervalMs != intervalMs)
  371. {
  372. lock (_notificationTimerSyncLock)
  373. {
  374. if (_notificationTimer == null)
  375. {
  376. _logger.Debug("Starting alive notifier");
  377. const int initialDelayMs = 3000;
  378. _notificationTimer = new Timer(state => NotifyAll(), null, initialDelayMs, intervalMs);
  379. }
  380. else
  381. {
  382. _logger.Debug("Updating alive notifier");
  383. _notificationTimer.Change(intervalMs, intervalMs);
  384. }
  385. _aliveNotifierIntervalMs = intervalMs;
  386. }
  387. }
  388. }
  389. private void DisposeNotificationTimer()
  390. {
  391. lock (_notificationTimerSyncLock)
  392. {
  393. if (_notificationTimer != null)
  394. {
  395. _logger.Debug("Stopping alive notifier");
  396. _notificationTimer.Dispose();
  397. _notificationTimer = null;
  398. }
  399. }
  400. }
  401. }
  402. }