diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 5ac701f82..3bc22c07f 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Common.Net /// true if [is in local network] [the specified endpoint]; otherwise, false. bool IsInLocalNetwork(string endpoint); - IEnumerable GetLocalIpAddresses(); + List GetLocalIpAddresses(); IpAddressInfo ParseIpAddress(string ipAddress); diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 599292ddf..ac406e7f1 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Net /// /// Createa a new unicast socket using the specified local port number. /// - IUdpSocket CreateSsdpUdpSocket(int localPort); + IUdpSocket CreateSsdpUdpSocket(IpAddressInfo localIp, int localPort); /// /// Createa a new multicast socket using the specified multicast IP address, multicast time to live and local port. diff --git a/MediaBrowser.Model/Net/IUdpSocket.cs b/MediaBrowser.Model/Net/IUdpSocket.cs index ef090e010..c70510726 100644 --- a/MediaBrowser.Model/Net/IUdpSocket.cs +++ b/MediaBrowser.Model/Net/IUdpSocket.cs @@ -10,16 +10,18 @@ namespace MediaBrowser.Model.Net /// Provides a common interface across platforms for UDP sockets used by this SSDP implementation. /// public interface IUdpSocket : IDisposable - { - /// - /// Waits for and returns the next UDP message sent to this socket (uni or multicast). - /// - /// - Task ReceiveAsync(); + { + IpAddressInfo LocalIPAddress { get; } + + /// + /// Waits for and returns the next UDP message sent to this socket (uni or multicast). + /// + /// + Task ReceiveAsync(); /// /// Sends a UDP message to a particular end point (uni or multicast). /// Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint); - } + } } \ No newline at end of file diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs index 0a2d04ad3..483e2297b 100644 --- a/MediaBrowser.Model/Net/SocketReceiveResult.cs +++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs @@ -20,5 +20,6 @@ namespace MediaBrowser.Model.Net /// The the data was received from. /// public IpEndPointInfo RemoteEndPoint { get; set; } - } + public IpAddressInfo LocalIPAddress { get; set; } + } } diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index 462f33fe6..eea5e0ed6 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -46,7 +46,8 @@ namespace Rssdp.Infrastructure /// /// A byte array containing the data to send. /// A representing the destination address for the data. Can be either a multicast or unicast destination. - Task SendMessage(byte[] messageData, IpEndPointInfo destination); + /// A The local ip address to send from, or .Any if sending from all available + Task SendMessage(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress); /// /// Sends a message to the SSDP multicast address and port. diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index d60f6ea44..ef1f32207 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -50,7 +50,6 @@ - @@ -70,6 +69,10 @@ + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} MediaBrowser.Model diff --git a/RSSDP/ReadOnlyEnumerable.cs b/RSSDP/ReadOnlyEnumerable.cs deleted file mode 100644 index 1a69f8837..000000000 --- a/RSSDP/ReadOnlyEnumerable.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Rssdp -{ - internal sealed class ReadOnlyEnumerable : System.Collections.Generic.IEnumerable - { - - #region Fields - - private IEnumerable _Items; - - #endregion - - #region Constructors - - public ReadOnlyEnumerable(IEnumerable items) - { - if (items == null) throw new ArgumentNullException("items"); - - _Items = items; - } - - #endregion - - #region IEnumerable Members - - public IEnumerator GetEnumerator() - { - return _Items.GetEnumerator(); - } - - #endregion - - #region IEnumerable Members - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return _Items.GetEnumerator(); - } - - #endregion - } -} diff --git a/RSSDP/RequestReceivedEventArgs.cs b/RSSDP/RequestReceivedEventArgs.cs index fb4fac871..03c059634 100644 --- a/RSSDP/RequestReceivedEventArgs.cs +++ b/RSSDP/RequestReceivedEventArgs.cs @@ -22,17 +22,18 @@ namespace Rssdp.Infrastructure #endregion - #region Constructors + public IpAddressInfo LocalIpAddress { get; private set; } + + #region Constructors /// /// Full constructor. /// - /// The that was received. - /// A representing the sender's address (sometimes used for replies). - public RequestReceivedEventArgs(HttpRequestMessage message, IpEndPointInfo receivedFrom) + public RequestReceivedEventArgs(HttpRequestMessage message, IpEndPointInfo receivedFrom, IpAddressInfo localIpAddress) { _Message = message; _ReceivedFrom = receivedFrom; + LocalIpAddress = localIpAddress; } #endregion diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 4de47f3d3..5d8ca15bb 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -5,6 +5,8 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; namespace Rssdp.Infrastructure @@ -38,12 +40,13 @@ namespace Rssdp.Infrastructure private IUdpSocket _BroadcastListenSocket; private object _SendSocketSynchroniser = new object(); - private IUdpSocket _SendSocket; + private List _sendSockets; private HttpRequestParser _RequestParser; private HttpResponseParser _ResponseParser; - + private readonly ILogger _logger; private ISocketFactory _SocketFactory; + private readonly INetworkManager _networkManager; private int _LocalPort; private int _MulticastTtl; @@ -71,22 +74,18 @@ namespace Rssdp.Infrastructure /// /// Minimum constructor. /// - /// An implementation of the interface that can be used to make new unicast and multicast sockets. Cannot be null. /// The argument is null. - public SsdpCommunicationsServer(ISocketFactory socketFactory) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive) + public SsdpCommunicationsServer(ISocketFactory socketFactory, INetworkManager networkManager, ILogger logger) + : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) { } /// /// Full constructor. /// - /// An implementation of the interface that can be used to make new unicast and multicast sockets. Cannot be null. - /// The specific local port to use for all sockets created by this instance. Specify zero to indicate the system should choose a free port itself. - /// The multicast time to live value for multicast sockets. Technically this is a number of router hops, not a 'Time'. Must be greater than zero. /// The argument is null. /// The argument is less than or equal to zero. - public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive) + public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger) { if (socketFactory == null) throw new ArgumentNullException("socketFactory"); if (multicastTimeToLive <= 0) throw new ArgumentOutOfRangeException("multicastTimeToLive", "multicastTimeToLive must be greater than zero."); @@ -101,6 +100,8 @@ namespace Rssdp.Infrastructure _ResponseParser = new HttpResponseParser(); _MulticastTtl = multicastTimeToLive; + _networkManager = networkManager; + _logger = logger; } #endregion @@ -148,25 +149,72 @@ namespace Rssdp.Infrastructure /// /// A byte array containing the data to send. /// A representing the destination address for the data. Can be either a multicast or unicast destination. + /// A The local ip address to send from, or .Any if sending from all available /// Thrown if the argument is null. /// Thrown if the property is true (because has been called previously). - public async Task SendMessage(byte[] messageData, IpEndPointInfo destination) + public async Task SendMessage(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress) { if (messageData == null) throw new ArgumentNullException("messageData"); ThrowIfDisposed(); - EnsureSendSocketCreated(); + var sockets = GetSendSockets(fromLocalIpAddress, destination); + + if (sockets.Count == 0) + { + return; + } // SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP. for (var i = 0; i < SsdpConstants.UdpResendCount; i++) { - await SendMessageIfSocketNotDisposed(messageData, destination).ConfigureAwait(false); + var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination)).ToArray(); + await Task.WhenAll(tasks).ConfigureAwait(false); await Task.Delay(100).ConfigureAwait(false); } } + private async Task SendFromSocket(IUdpSocket socket, byte[] messageData, IpEndPointInfo destination) + { + try + { + await socket.SendAsync(messageData, messageData.Length, destination).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending socket message from {0} to {1}", ex, socket.LocalIPAddress.ToString(), destination.ToString()); + } + } + + private List GetSendSockets(IpAddressInfo fromLocalIpAddress, IpEndPointInfo destination) + { + EnsureSendSocketCreated(); + + lock (_SendSocketSynchroniser) + { + var sockets = _sendSockets.Where(i => i.LocalIPAddress.AddressFamily == fromLocalIpAddress.AddressFamily); + + // Send from the Any socket and the socket with the matching address + if (fromLocalIpAddress.AddressFamily == IpAddressFamily.InterNetwork) + { + sockets = sockets.Where(i => i.LocalIPAddress.Equals(IpAddressInfo.Any) || fromLocalIpAddress.Equals(i.LocalIPAddress)); + } + else if (fromLocalIpAddress.AddressFamily == IpAddressFamily.InterNetworkV6) + { + sockets = sockets.Where(i => i.LocalIPAddress.Equals(IpAddressInfo.IPv6Any) || fromLocalIpAddress.Equals(i.LocalIPAddress)); + } + + // If sending to the loopback address, filter the socket list as well + if (destination.IpAddress.Equals(IpAddressInfo.Loopback)) + { + sockets = sockets.Where(i => i.LocalIPAddress.Equals(IpAddressInfo.Any) || i.LocalIPAddress.Equals(IpAddressInfo.Loopback)); + } + + return sockets.ToList(); + } + } + /// /// Sends a message to the SSDP multicast address and port. /// @@ -185,7 +233,10 @@ namespace Rssdp.Infrastructure { await SendMessageIfSocketNotDisposed(messageData, new IpEndPointInfo { - IpAddress = new IpAddressInfo { Address = SsdpConstants.MulticastLocalAdminAddress }, + IpAddress = new IpAddressInfo + { + Address = SsdpConstants.MulticastLocalAdminAddress + }, Port = SsdpConstants.MulticastPort }).ConfigureAwait(false); @@ -204,10 +255,16 @@ namespace Rssdp.Infrastructure lock (_SendSocketSynchroniser) { - var socket = _SendSocket; - _SendSocket = null; - if (socket != null) - socket.Dispose(); + if (_sendSockets != null) + { + var sockets = _sendSockets.ToList(); + _sendSockets = null; + + foreach (var socket in sockets) + { + socket.Dispose(); + } + } } } @@ -247,8 +304,16 @@ namespace Rssdp.Infrastructure lock (_SendSocketSynchroniser) { - if (_SendSocket != null) - _SendSocket.Dispose(); + if (_sendSockets != null) + { + var sockets = _sendSockets.ToList(); + _sendSockets = null; + + foreach (var socket in sockets) + { + socket.Dispose(); + } + } } } } @@ -257,16 +322,20 @@ namespace Rssdp.Infrastructure #region Private Methods - private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination) + private async Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination) { - var socket = _SendSocket; - if (socket != null) + var sockets = _sendSockets; + if (sockets != null) { - return _SendSocket.SendAsync(messageData, messageData.Length, destination); + sockets = sockets.ToList(); + + foreach (var socket in sockets) + { + await socket.SendAsync(messageData, messageData.Length, destination).ConfigureAwait(false); + } } ThrowIfDisposed(); - return Task.FromResult(true); } private IUdpSocket ListenForBroadcastsAsync() @@ -278,13 +347,30 @@ namespace Rssdp.Infrastructure return socket; } - private IUdpSocket CreateSocketAndListenForResponsesAsync() + private List CreateSocketAndListenForResponsesAsync() { - _SendSocket = _SocketFactory.CreateSsdpUdpSocket(_LocalPort); + var sockets = new List(); - ListenToSocket(_SendSocket); + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(IpAddressInfo.Any, _LocalPort)); - return _SendSocket; + foreach (var address in _networkManager.GetLocalIpAddresses().ToList()) + { + try + { + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort)); + } + catch (Exception ex) + { + _logger.ErrorException("Error in CreateSsdpUdpSocket. IPAddress: {0}", ex, address); + } + } + + foreach (var socket in sockets) + { + ListenToSocket(socket); + } + + return sockets; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capturing task to local variable removes compiler warning, task is not otherwise required.")] @@ -305,7 +391,7 @@ namespace Rssdp.Infrastructure // Strange cannot convert compiler error here if I don't explicitly // assign or cast to Action first. Assignment is easier to read, // so went with that. - ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint); + ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress); } } catch (ObjectDisposedException) @@ -322,19 +408,19 @@ namespace Rssdp.Infrastructure private void EnsureSendSocketCreated() { - if (_SendSocket == null) + if (_sendSockets == null) { lock (_SendSocketSynchroniser) { - if (_SendSocket == null) + if (_sendSockets == null) { - _SendSocket = CreateSocketAndListenForResponsesAsync(); + _sendSockets = CreateSocketAndListenForResponsesAsync(); } } } } - private void ProcessMessage(string data, IpEndPointInfo endPoint) + private void ProcessMessage(string data, IpEndPointInfo endPoint, IpAddressInfo receivedOnLocalIpAddress) { //Responses start with the HTTP version, prefixed with HTTP/ while //requests start with a method which can vary and might be one we haven't @@ -347,7 +433,10 @@ namespace Rssdp.Infrastructure { responseMessage = _ResponseParser.Parse(data); } - catch (ArgumentException) { } // Ignore invalid packets. + catch (ArgumentException ex) + { + // Ignore invalid packets. + } if (responseMessage != null) OnResponseReceived(responseMessage, endPoint); @@ -359,23 +448,31 @@ namespace Rssdp.Infrastructure { requestMessage = _RequestParser.Parse(data); } - catch (ArgumentException) { } // Ignore invalid packets. + catch (ArgumentException ex) + { + // Ignore invalid packets. + } if (requestMessage != null) - OnRequestReceived(requestMessage, endPoint); + { + OnRequestReceived(requestMessage, endPoint, receivedOnLocalIpAddress); + } } } - private void OnRequestReceived(HttpRequestMessage data, IpEndPointInfo endPoint) + private void OnRequestReceived(HttpRequestMessage data, IpEndPointInfo remoteEndPoint, IpAddressInfo receivedOnLocalIpAddress) { //SSDP specification says only * is currently used but other uri's might //be implemented in the future and should be ignored unless understood. //Section 4.2 - http://tools.ietf.org/html/draft-cai-ssdp-v1-03#page-11 - if (data.RequestUri.ToString() != "*") return; + if (data.RequestUri.ToString() != "*") + { + return; + } var handlers = this.RequestReceived; if (handlers != null) - handlers(this, new RequestReceivedEventArgs(data, endPoint)); + handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnLocalIpAddress)); } private void OnResponseReceived(HttpResponseMessage data, IpEndPointInfo endPoint) diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs index 8a4992239..a595742d0 100644 --- a/RSSDP/SsdpDevice.cs +++ b/RSSDP/SsdpDevice.cs @@ -64,7 +64,7 @@ namespace Rssdp this.Icons = new List(); _Devices = new List(); - this.Devices = new ReadOnlyEnumerable(_Devices); + this.Devices = new ReadOnlyCollection(_Devices); _CustomResponseHeaders = new CustomHttpHeadersCollection(); _CustomProperties = new SsdpDevicePropertiesCollection(); } diff --git a/RSSDP/SsdpDevicePublisherBase.cs b/RSSDP/SsdpDevicePublisherBase.cs index 7737733f7..ee3335957 100644 --- a/RSSDP/SsdpDevicePublisherBase.cs +++ b/RSSDP/SsdpDevicePublisherBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Text; @@ -25,7 +26,7 @@ namespace Rssdp.Infrastructure private bool _SupportPnpRootDevice; private IList _Devices; - private ReadOnlyEnumerable _ReadOnlyDevices; + private IReadOnlyList _ReadOnlyDevices; private ITimer _RebroadcastAliveNotificationsTimer; private ITimerFactory _timerFactory; @@ -62,7 +63,7 @@ namespace Rssdp.Infrastructure _SupportPnpRootDevice = true; _timerFactory = timerFactory; _Devices = new List(); - _ReadOnlyDevices = new ReadOnlyEnumerable(_Devices); + _ReadOnlyDevices = new ReadOnlyCollection(_Devices); _RecentSearchRequests = new Dictionary(StringComparer.OrdinalIgnoreCase); _Random = new Random(); _DeviceValidator = new Upnp10DeviceValidator(); //Should probably inject this later, but for now we only support 1.0. @@ -236,17 +237,17 @@ namespace Rssdp.Infrastructure #region Search Related Methods - private void ProcessSearchRequest(string mx, string searchTarget, IpEndPointInfo endPoint) + private void ProcessSearchRequest(string mx, string searchTarget, IpEndPointInfo remoteEndPoint, IpAddressInfo receivedOnlocalIpAddress) { if (String.IsNullOrEmpty(searchTarget)) { - WriteTrace(String.Format("Invalid search request received From {0}, Target is null/empty.", endPoint.ToString())); + WriteTrace(String.Format("Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); return; } - WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", endPoint.ToString(), searchTarget)); + WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", remoteEndPoint.ToString(), searchTarget)); - if (IsDuplicateSearchRequest(searchTarget, endPoint)) + if (IsDuplicateSearchRequest(searchTarget, remoteEndPoint)) { WriteTrace("Search Request is Duplicate, ignoring."); return; @@ -294,7 +295,7 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - SendDeviceSearchResponses(device, endPoint); + SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress); } } else @@ -307,19 +308,19 @@ namespace Rssdp.Infrastructure return _Devices.Union(_Devices.SelectManyRecursive((d) => d.Devices)); } - private void SendDeviceSearchResponses(SsdpDevice device, IpEndPointInfo endPoint) + private void SendDeviceSearchResponses(SsdpDevice device, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress) { bool isRootDevice = (device as SsdpRootDevice) != null; if (isRootDevice) { - SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint); + SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress); if (this.SupportPnpRootDevice) - SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint); + SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress); } - SendSearchResponse(device.Udn, device, device.Udn, endPoint); + SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIpAddress); - SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint); + SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIpAddress); } private static string GetUsn(string udn, string fullDeviceType) @@ -327,7 +328,7 @@ namespace Rssdp.Infrastructure return String.Format("{0}::{1}", udn, fullDeviceType); } - private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, IpEndPointInfo endPoint) + private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress) { var rootDevice = device.ToRootDevice(); @@ -349,7 +350,7 @@ namespace Rssdp.Infrastructure try { - await _CommsServer.SendMessage(System.Text.Encoding.UTF8.GetBytes(message), endPoint).ConfigureAwait(false); + await _CommsServer.SendMessage(System.Text.Encoding.UTF8.GetBytes(message), endPoint, receivedOnlocalIpAddress).ConfigureAwait(false); } catch (Exception ex) { @@ -674,7 +675,7 @@ namespace Rssdp.Infrastructure //else if (!e.Message.Headers.Contains("MAN")) // WriteTrace("Ignoring search request - missing MAN header."); //else - ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom); + ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom, e.LocalIpAddress); } }