diff --git a/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs index e8083b363..d2c5b9e4e 100644 --- a/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs +++ b/MediaBrowser.Controller/Dlna/IDeviceDiscovery.cs @@ -1,10 +1,20 @@ using System; +using System.Collections.Generic; +using System.Net; +using MediaBrowser.Model.Events; namespace MediaBrowser.Controller.Dlna { public interface IDeviceDiscovery { - event EventHandler DeviceDiscovered; - event EventHandler DeviceLeft; + event EventHandler> DeviceDiscovered; + event EventHandler> DeviceLeft; + } + + public class UpnpDeviceInfo + { + public Uri Location { get; set; } + public Dictionary Headers { get; set; } + public IPEndPoint LocalEndPoint { get; set; } } } diff --git a/MediaBrowser.Controller/Dlna/ISsdpHandler.cs b/MediaBrowser.Controller/Dlna/ISsdpHandler.cs index e4126ddcf..ec3a00aad 100644 --- a/MediaBrowser.Controller/Dlna/ISsdpHandler.cs +++ b/MediaBrowser.Controller/Dlna/ISsdpHandler.cs @@ -4,6 +4,5 @@ namespace MediaBrowser.Controller.Dlna { public interface ISsdpHandler { - event EventHandler MessageReceived; } } diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index ea5e6dbc6..d0377fbfd 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -107,6 +107,8 @@ namespace MediaBrowser.Controller.LiveTv /// The image URL. public string ImageUrl { get; set; } + public string LogoImageUrl { get; set; } + /// /// Gets or sets a value indicating whether this instance has image. /// diff --git a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs index 9f2726b31..af03f325f 100644 --- a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs +++ b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs @@ -14,8 +14,10 @@ using MediaBrowser.Dlna.Ssdp; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using Rssdp; namespace MediaBrowser.Dlna.Main { @@ -38,12 +40,11 @@ namespace MediaBrowser.Dlna.Main private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; - private readonly SsdpHandler _ssdpHandler; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly List _registeredServerIds = new List(); private bool _ssdpHandlerStarted; private bool _dlnaServerStarted; + private SsdpDevicePublisher _Publisher; public DlnaEntryPoint(IServerConfigurationManager config, ILogManager logManager, @@ -58,7 +59,7 @@ namespace MediaBrowser.Dlna.Main IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - ISsdpHandler ssdpHandler, IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder) + IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder) { _config = config; _appHost = appHost; @@ -74,7 +75,6 @@ namespace MediaBrowser.Dlna.Main _mediaSourceManager = mediaSourceManager; _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; - _ssdpHandler = (SsdpHandler)ssdpHandler; _logger = logManager.GetLogger("Dlna"); } @@ -154,7 +154,7 @@ namespace MediaBrowser.Dlna.Main { try { - _ssdpHandler.Start(); + StartPublishing(); _ssdpHandlerStarted = true; StartDeviceDiscovery(); @@ -165,13 +165,16 @@ namespace MediaBrowser.Dlna.Main } } + private void StartPublishing() + { + _Publisher = new SsdpDevicePublisher(); + } + private void StartDeviceDiscovery() { try { - ((DeviceDiscovery)_deviceDiscovery).Start(_ssdpHandler); - - //DlnaChannel.Current.Start(() => _registeredServerIds.ToList()); + ((DeviceDiscovery)_deviceDiscovery).Start(); } catch (Exception ex) { @@ -199,8 +202,6 @@ namespace MediaBrowser.Dlna.Main { ((DeviceDiscovery)_deviceDiscovery).Dispose(); - _ssdpHandler.Dispose(); - _ssdpHandlerStarted = false; } catch (Exception ex) @@ -225,6 +226,14 @@ namespace MediaBrowser.Dlna.Main private async Task RegisterServerEndpoints() { + if (!_config.GetDlnaConfiguration().BlastAliveMessages) + { + return; + } + + var cacheLength = _config.GetDlnaConfiguration().BlastAliveMessageIntervalSeconds*2; + _Publisher.SupportPnpRootDevice = true; + foreach (var address in await _appHost.GetLocalIpAddresses().ConfigureAwait(false)) { //if (IPAddress.IsLoopback(address)) @@ -234,25 +243,41 @@ namespace MediaBrowser.Dlna.Main //} var addressString = address.ToString(); - var udn = addressString.GetMD5().ToString("N"); - - var descriptorURI = "/dlna/" + udn + "/description.xml"; - - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorURI); var services = new List { - "upnp:rootdevice", "urn:schemas-upnp-org:device:MediaServer:1", "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", - "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", - "uuid:" + udn + "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" }; - _ssdpHandler.RegisterNotification(udn, uri, address, services); + var udn = (addressString).GetMD5().ToString("N"); - _registeredServerIds.Add(udn); + foreach (var fullService in services) + { + var descriptorURI = "/dlna/" + udn + "/description.xml"; + var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorURI); + + var service = fullService.Replace("urn:", string.Empty).Replace(":1", string.Empty); + + var serviceParts = service.Split(':'); + + var deviceTypeNamespace = serviceParts[0].Replace('.', '-'); + + _Publisher.AddDevice(new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(cacheLength), //How long SSDP clients can cache this info. + Location = uri, // Must point to the URL that serves your devices UPnP description document. + DeviceTypeNamespace = deviceTypeNamespace, + DeviceClass = serviceParts[1], + DeviceType = serviceParts[2], + FriendlyName = "Emby Server", + Manufacturer = "Emby", + ModelName = "Emby Server", + Uuid = udn // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }); + } } } @@ -315,20 +340,23 @@ namespace MediaBrowser.Dlna.Main public void DisposeDlnaServer() { - foreach (var id in _registeredServerIds) + if (_Publisher != null) { - try + var devices = _Publisher.Devices.ToList(); + foreach (var device in devices) { - _ssdpHandler.UnregisterNotification(id); - } - catch (Exception ex) - { - _logger.ErrorException("Error unregistering server", ex); + try + { + _Publisher.RemoveDevice(device); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending bye bye", ex); + } } + _Publisher.Dispose(); } - _registeredServerIds.Clear(); - _dlnaServerStarted = false; } } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index d10a5f7b5..ae2e43a4f 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -1,5 +1,5 @@  - + Debug @@ -14,6 +14,7 @@ 2.0 v4.5 ..\ + true @@ -23,7 +24,7 @@ DEBUG;TRACE prompt 4 - v4.5 + v4.5.1 none @@ -32,7 +33,7 @@ TRACE prompt 4 - v4.5 + v4.5.1 false @@ -50,8 +51,15 @@ ..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll + + ..\ThirdParty\rssdp\Rssdp.NetFx40.dll + + + ..\ThirdParty\rssdp\Rssdp.Portable.dll + + diff --git a/MediaBrowser.Dlna/PlayTo/PlayToController.cs b/MediaBrowser.Dlna/PlayTo/PlayToController.cs index 5622885fc..d958d0e37 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToController.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToController.cs @@ -19,6 +19,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Events; namespace MediaBrowser.Dlna.PlayTo { @@ -122,16 +123,18 @@ namespace MediaBrowser.Dlna.PlayTo } } - void _deviceDiscovery_DeviceLeft(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs e) { + var info = e.Argument; + string nts; - e.Headers.TryGetValue("NTS", out nts); + info.Headers.TryGetValue("NTS", out nts); string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = String.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = String.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = String.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = String.Empty; if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 && !_disposed) @@ -653,7 +656,7 @@ namespace MediaBrowser.Dlna.PlayTo _device.PlaybackProgress -= _device_PlaybackProgress; _device.PlaybackStopped -= _device_PlaybackStopped; _device.MediaChanged -= _device_MediaChanged; - _deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; + //_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; _device.OnDeviceUnavailable = null; _device.Dispose(); diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index cd9a7b1f0..6d6986f01 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -12,7 +12,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Events; namespace MediaBrowser.Dlna.PlayTo { @@ -61,16 +63,17 @@ namespace MediaBrowser.Dlna.PlayTo _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - async void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { + var info = e.Argument; + string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; - string location; - if (!e.Headers.TryGetValue("Location", out location)) location = string.Empty; + string location = info.Location.ToString(); // It has to report that it's a media renderer if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && @@ -100,7 +103,7 @@ namespace MediaBrowser.Dlna.PlayTo } } - var uri = new Uri(location); + var uri = info.Location; _logger.Debug("Attempting to create PlayToController from location {0}", location); var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger).ConfigureAwait(false); @@ -121,7 +124,7 @@ namespace MediaBrowser.Dlna.PlayTo if (controller == null) { - var serverAddress = GetServerAddress(e.LocalEndPoint.Address); + var serverAddress = await GetServerAddress(info.LocalEndPoint == null ? null : info.LocalEndPoint.Address).ConfigureAwait(false); string accessToken = null; sessionInfo.SessionController = controller = new PlayToController(sessionInfo, @@ -173,9 +176,14 @@ namespace MediaBrowser.Dlna.PlayTo } } - private string GetServerAddress(IPAddress localIp) + private Task GetServerAddress(IPAddress localIp) { - return _appHost.GetLocalApiUrl(localIp); + if (localIp == null) + { + return _appHost.GetLocalApiUrl(); + } + + return Task.FromResult(_appHost.GetLocalApiUrl(localIp)); } public void Dispose() diff --git a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs index 68768745e..91dbeb96e 100644 --- a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs +++ b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs @@ -11,6 +11,8 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Events; +using Rssdp; namespace MediaBrowser.Dlna.Ssdp { @@ -20,132 +22,43 @@ namespace MediaBrowser.Dlna.Ssdp private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private SsdpHandler _ssdpHandler; private readonly CancellationTokenSource _tokenSource; - private readonly IServerApplicationHost _appHost; - public event EventHandler DeviceDiscovered; - public event EventHandler DeviceLeft; - private readonly INetworkManager _networkManager; + public event EventHandler> DeviceDiscovered; + public event EventHandler> DeviceLeft; - public DeviceDiscovery(ILogger logger, IServerConfigurationManager config, IServerApplicationHost appHost, INetworkManager networkManager) + private SsdpDeviceLocator _DeviceLocator; + + public DeviceDiscovery(ILogger logger, IServerConfigurationManager config) { _tokenSource = new CancellationTokenSource(); _logger = logger; _config = config; - _appHost = appHost; - _networkManager = networkManager; } - private List GetLocalIpAddresses() - { - return _networkManager.GetLocalIpAddresses().ToList(); - } - - public void Start(SsdpHandler ssdpHandler) + // Call this method from somewhere in your code to start the search. + public void BeginSearch() { - _ssdpHandler = ssdpHandler; - _ssdpHandler.MessageReceived += _ssdpHandler_MessageReceived; + _DeviceLocator = new SsdpDeviceLocator(); - foreach (var localIp in GetLocalIpAddresses()) - { - try - { - CreateListener(localIp); - } - catch (Exception e) - { - _logger.ErrorException("Failed to Initilize Socket", e); - } - } + // (Optional) Set the filter so we only see notifications for devices we care about + // (can be any search target value i.e device type, uuid value etc - any value that appears in the + // DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method). + //_DeviceLocator.NotificationFilter = "upnp:rootdevice"; + + // Connect our event handler so we process devices as they are found + _DeviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; + _DeviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; + // Enable listening for notifications (optional) + _DeviceLocator.StartListeningForNotifications(); + + // Perform a search so we don't have to wait for devices to broadcast notifications + // again to get any results right away (notifications are broadcast periodically). + StartAsyncSearch(); } - async void _ssdpHandler_MessageReceived(object sender, SsdpMessageEventArgs e) - { - string nts; - e.Headers.TryGetValue("NTS", out nts); - - if (String.Equals(e.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase) && - String.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase) && - !_disposed) - { - EventHelper.FireEventIfNotNull(DeviceLeft, this, e, _logger); - return; - } - - try - { - if (e.LocalEndPoint == null) - { - var ip = (await _appHost.GetLocalIpAddresses().ConfigureAwait(false)).FirstOrDefault(i => !IPAddress.IsLoopback(i)); - if (ip != null) - { - e.LocalEndPoint = new IPEndPoint(ip, 0); - } - } - - if (e.LocalEndPoint != null) - { - TryCreateDevice(e); - } - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error creating play to controller", ex); - } - } - - private void CreateListener(IPAddress localIp) - { - Task.Factory.StartNew(async (o) => - { - try - { - _logger.Info("Creating SSDP listener on {0}", localIp); - - var endPoint = new IPEndPoint(localIp, 1900); - - using (var socket = GetMulticastSocket(localIp, endPoint)) - { - var receiveBuffer = new byte[64000]; - - CreateNotifier(localIp); - - while (!_tokenSource.IsCancellationRequested) - { - var receivedBytes = await socket.ReceiveAsync(receiveBuffer, 0, 64000); - - if (receivedBytes > 0) - { - var args = SsdpHelper.ParseSsdpResponse(receiveBuffer); - args.EndPoint = endPoint; - args.LocalEndPoint = new IPEndPoint(localIp, 0); - - _ssdpHandler.LogMessageReceived(args, true); - - TryCreateDevice(args); - } - } - } - - _logger.Info("SSDP listener - Task completed"); - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - _logger.ErrorException("Error in listener", e); - } - - }, _tokenSource.Token, TaskCreationOptions.LongRunning); - } - - private void CreateNotifier(IPAddress localIp) + private void StartAsyncSearch() { Task.Factory.StartNew(async (o) => { @@ -153,7 +66,7 @@ namespace MediaBrowser.Dlna.Ssdp { while (true) { - _ssdpHandler.SendSearchMessage(new IPEndPoint(localIp, 1900)); + await _DeviceLocator.SearchAsync().ConfigureAwait(false); var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000; @@ -165,66 +78,60 @@ namespace MediaBrowser.Dlna.Ssdp } catch (Exception ex) { - _logger.ErrorException("Error in notifier", ex); + _logger.ErrorException("Error searching for devices", ex); } - }, _tokenSource.Token, TaskCreationOptions.LongRunning); + }, CancellationToken.None, TaskCreationOptions.LongRunning); } - private Socket GetMulticastSocket(IPAddress localIpAddress, EndPoint localEndpoint) + // Process each found device in the event handler + void deviceLocator_DeviceAvailable(object sender, DeviceAvailableEventArgs e) { - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIpAddress)); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + var originalHeaders = e.DiscoveredDevice.ResponseHeaders; - socket.Bind(localEndpoint); + var headerDict = originalHeaders == null ? new Dictionary>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); - return socket; - } + var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - private void TryCreateDevice(SsdpMessageEventArgs args) - { - string nts; - args.Headers.TryGetValue("NTS", out nts); - - if (String.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase)) + var args = new GenericEventArgs { - if (String.Equals(args.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase)) + Argument = new UpnpDeviceInfo { - if (!_disposed) - { - EventHelper.FireEventIfNotNull(DeviceLeft, this, args, _logger); - } + Location = e.DiscoveredDevice.DescriptionLocation, + Headers = headers } - - return; - } - - string usn; - if (!args.Headers.TryGetValue("USN", out usn)) usn = string.Empty; - - string nt; - if (!args.Headers.TryGetValue("NT", out nt)) nt = string.Empty; - - // Need to be able to download device description - string location; - if (!args.Headers.TryGetValue("Location", out location) || - string.IsNullOrEmpty(location)) - { - return; - } + }; EventHelper.FireEventIfNotNull(DeviceDiscovered, this, args, _logger); } + private void _DeviceLocator_DeviceUnavailable(object sender, DeviceUnavailableEventArgs e) + { + var originalHeaders = e.DiscoveredDevice.ResponseHeaders; + + var headerDict = originalHeaders == null ? new Dictionary>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase); + + var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + + var args = new GenericEventArgs + { + Argument = new UpnpDeviceInfo + { + Location = e.DiscoveredDevice.DescriptionLocation, + Headers = headers + } + }; + + EventHelper.FireEventIfNotNull(DeviceLeft, this, args, _logger); + } + + public void Start() + { + BeginSearch(); + } + public void Dispose() { - if (_ssdpHandler != null) - { - _ssdpHandler.MessageReceived -= _ssdpHandler_MessageReceived; - } - if (!_disposed) { _disposed = true; diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index 720ea71a0..0d0ca98a2 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -83,90 +83,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - public event EventHandler MessageReceived; - - private async void OnMessageReceived(SsdpMessageEventArgs args, bool isMulticast) - { - if (IgnoreMessage(args, isMulticast)) - { - return; - } - - LogMessageReceived(args, isMulticast); - - var headers = args.Headers; - string st; - - if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase) && headers.TryGetValue("st", out st)) - { - TimeSpan delay = GetSearchDelay(headers); - - if (_config.GetDlnaConfiguration().EnableDebugLog) - { - _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds); - } - - await Task.Delay(delay).ConfigureAwait(false); - - RespondToSearch(args.EndPoint, st); - } - - EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger); - } - - internal void LogMessageReceived(SsdpMessageEventArgs args, bool isMulticast) - { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - var headerText = string.Join(",", headerTexts.ToArray()); - - var protocol = isMulticast ? "Multicast" : "Unicast"; - var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - _logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - } - } - - internal bool IgnoreMessage(SsdpMessageEventArgs args, bool isMulticast) - { - string usn; - if (args.Headers.TryGetValue("USN", out usn)) - { - // USN=uuid:b67df29b5c379445fde78c3774ab518d::urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1 - if (RegisteredDevices.Any(i => string.Equals(i.USN, usn, StringComparison.OrdinalIgnoreCase))) - { - //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - //var headerText = string.Join(",", headerTexts.ToArray()); - - //var protocol = isMulticast ? "Multicast" : "Unicast"; - //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - - return true; - } - } - - string serverId; - if (args.Headers.TryGetValue("X-EMBY-SERVERID", out serverId)) - { - if (string.Equals(serverId, _appHost.SystemId, StringComparison.OrdinalIgnoreCase)) - { - //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); - //var headerText = string.Join(",", headerTexts.ToArray()); - - //var protocol = isMulticast ? "Multicast" : "Unicast"; - //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString(); - //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol); - - return true; - } - } - - return false; - } - public IEnumerable RegisteredDevices { get @@ -188,8 +104,6 @@ namespace MediaBrowser.Dlna.Ssdp RestartSocketListener(); ReloadAliveNotifier(); - CreateUnicastClient(); - SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -202,32 +116,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - public void SendSearchMessage(EndPoint localIp) - { - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["HOST"] = "239.255.255.250:1900"; - values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; - values["X-EMBY-SERVERID"] = _appHost.SystemId; - - values["MAN"] = "\"ssdp:discover\""; - - // Search target - values["ST"] = "ssdp:all"; - - // Seconds to delay response - values["MX"] = "3"; - - var header = "M-SEARCH * HTTP/1.1"; - - var msg = new SsdpMessageBuilder().BuildMessage(header, values); - - // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2) - SendDatagram(msg, _ssdpEndp, localIp, true); - - SendUnicastRequest(msg); - } - public async void SendDatagram(string msg, EndPoint endpoint, EndPoint localAddress, @@ -248,75 +136,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - /// - /// According to the spec: http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf - /// Device responses should be delayed a random duration between 0 and this many seconds to balance - /// load for the control point when it processes responses. In my testing kodi times out after mx - /// so we will generate from mx - 1 - /// - /// The mx headers - /// A timepsan for the amount to delay before returning search result. - private TimeSpan GetSearchDelay(Dictionary headers) - { - string mx; - headers.TryGetValue("mx", out mx); - int delaySeconds = 0; - if (!string.IsNullOrWhiteSpace(mx) - && int.TryParse(mx, NumberStyles.Any, CultureInfo.InvariantCulture, out delaySeconds) - && delaySeconds > 1) - { - delaySeconds = new Random().Next(delaySeconds - 1); - } - - return TimeSpan.FromSeconds(delaySeconds); - } - - private void RespondToSearch(EndPoint endpoint, string deviceType) - { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - var isLogged = false; - - const string header = "HTTP/1.1 200 OK"; - - foreach (var d in RegisteredDevices) - { - if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) || - string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase)) - { - if (!isLogged) - { - if (enableDebugLogging) - { - _logger.Debug("Responding to search from {0} for {1}", endpoint, deviceType); - } - isLogged = true; - } - - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["CACHE-CONTROL"] = "max-age = 600"; - values["DATE"] = DateTime.Now.ToString("R"); - values["EXT"] = ""; - values["LOCATION"] = d.Descriptor.ToString(); - values["SERVER"] = _serverSignature; - values["ST"] = d.Type; - values["USN"] = d.USN; - - var msg = new SsdpMessageBuilder().BuildMessage(header, values); - - SendDatagram(msg, endpoint, null, false, 2); - SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 2); - //SendDatagram(header, values, endpoint, null, true); - - if (enableDebugLogging) - { - _logger.Debug("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString()); - } - } - } - } - private void RestartSocketListener() { if (_isDisposed) @@ -329,8 +148,6 @@ namespace MediaBrowser.Dlna.Ssdp _multicastSocket = CreateMulticastSocket(); _logger.Info("MultiCast socket created"); - - Receive(); } catch (Exception ex) { @@ -339,74 +156,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - private void Receive() - { - try - { - var buffer = new byte[1024]; - - EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - - _multicastSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer); - } - catch (ObjectDisposedException) - { - if (!_isDisposed) - { - //StartSocketRetryTimer(); - } - } - catch (Exception ex) - { - _logger.Debug("Error in BeginReceiveFrom", ex); - } - } - - private void ReceiveCallback(IAsyncResult result) - { - if (_isDisposed) - { - return; - } - - try - { - EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - - var length = _multicastSocket.EndReceiveFrom(result, ref endpoint); - - var received = (byte[])result.AsyncState; - - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; - - if (enableDebugLogging) - { - _logger.Debug(Encoding.ASCII.GetString(received)); - } - - var args = SsdpHelper.ParseSsdpResponse(received); - args.EndPoint = endpoint; - - OnMessageReceived(args, true); - } - catch (ObjectDisposedException) - { - if (!_isDisposed) - { - //StartSocketRetryTimer(); - } - } - catch (Exception ex) - { - _logger.ErrorException("Failed to read SSDP message", ex); - } - - if (_multicastSocket != null) - { - Receive(); - } - } - public void Dispose() { _config.NamedConfigurationUpdated -= _config_ConfigurationUpdated; @@ -414,7 +163,6 @@ namespace MediaBrowser.Dlna.Ssdp _isDisposed = true; - DisposeUnicastClient(); DisposeSocket(); StopAliveNotifier(); } @@ -523,137 +271,6 @@ namespace MediaBrowser.Dlna.Ssdp } } - private void CreateUnicastClient() - { - if (_unicastClient == null) - { - try - { - _unicastClient = new UdpClient(_unicastPort); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating unicast client", ex); - } - - UnicastSetBeginReceive(); - } - } - - private void DisposeUnicastClient() - { - if (_unicastClient != null) - { - try - { - _unicastClient.Close(); - } - catch (Exception ex) - { - _logger.ErrorException("Error closing unicast client", ex); - } - - _unicastClient = null; - } - } - - /// - /// Listen for Unicast SSDP Responses - /// - private void UnicastSetBeginReceive() - { - try - { - var ipRxEnd = new IPEndPoint(IPAddress.Any, _unicastPort); - var udpListener = new UdpState { EndPoint = ipRxEnd }; - - udpListener.UdpClient = _unicastClient; - _unicastClient.BeginReceive(UnicastReceiveCallback, udpListener); - } - catch (Exception ex) - { - _logger.ErrorException("Error in UnicastSetBeginReceive", ex); - } - } - - /// - /// The UnicastReceiveCallback receives Http Responses - /// and Fired the SatIpDeviceFound Event for adding the SatIpDevice - /// - /// - private void UnicastReceiveCallback(IAsyncResult ar) - { - var udpClient = ((UdpState)(ar.AsyncState)).UdpClient; - var endpoint = ((UdpState)(ar.AsyncState)).EndPoint; - if (udpClient.Client != null) - { - try - { - var responseBytes = udpClient.EndReceive(ar, ref endpoint); - var args = SsdpHelper.ParseSsdpResponse(responseBytes); - - args.EndPoint = endpoint; - - OnMessageReceived(args, false); - - UnicastSetBeginReceive(); - } - catch (ObjectDisposedException) - { - - } - catch (SocketException) - { - - } - catch (Exception) - { - // If called while shutting down, seeing a NullReferenceException inside EndReceive - } - } - } - - private void SendUnicastRequest(string request, int sendCount = 3) - { - if (_unicastClient == null) - { - return; - } - - var ipSsdp = IPAddress.Parse(SSDPAddr); - var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort); - - SendUnicastRequest(request, ipTxEnd, sendCount); - } - - private async void SendUnicastRequest(string request, IPEndPoint toEndPoint, int sendCount = 3) - { - if (_unicastClient == null) - { - return; - } - - //_logger.Debug("Sending unicast request"); - - byte[] req = Encoding.ASCII.GetBytes(request); - - try - { - for (var i = 0; i < sendCount; i++) - { - if (i > 0) - { - await Task.Delay(50).ConfigureAwait(false); - } - _unicastClient.Send(req, req.Length, toEndPoint); - } - } - catch (Exception ex) - { - _logger.ErrorException("Error in SendUnicastRequest", ex); - } - } - private readonly object _notificationTimerSyncLock = new object(); private int _aliveNotifierIntervalMs; private void ReloadAliveNotifier() diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 351740e6e..ad7dea0a5 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -175,9 +175,6 @@ Configuration\AccessSchedule.cs - - Configuration\AutoOnOff.cs - Configuration\BaseApplicationConfiguration.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index ad3811646..61f2f3f13 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -147,9 +147,6 @@ Configuration\AccessSchedule.cs - - Configuration\AutoOnOff.cs - Configuration\BaseApplicationConfiguration.cs @@ -1193,4 +1190,4 @@ --> - + \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/AutoOnOff.cs b/MediaBrowser.Model/Configuration/AutoOnOff.cs deleted file mode 100644 index e911a0ff1..000000000 --- a/MediaBrowser.Model/Configuration/AutoOnOff.cs +++ /dev/null @@ -1,10 +0,0 @@ - -namespace MediaBrowser.Model.Configuration -{ - public enum AutoOnOff - { - Auto, - Enabled, - Disabled - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 40ac4be8a..5cf266674 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -183,8 +183,6 @@ namespace MediaBrowser.Model.Configuration public int RemoteClientBitrateLimit { get; set; } - public AutoOnOff EnableLibraryMonitor { get; set; } - public int SharingExpirationDays { get; set; } public string[] Migrations { get; set; } @@ -244,7 +242,6 @@ namespace MediaBrowser.Model.Configuration // 5 minutes MinResumeDurationSeconds = 300; - EnableLibraryMonitor = AutoOnOff.Auto; LibraryMonitorDelay = 60; EnableInternetProviders = true; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index db70b8606..c1a01680d 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -89,7 +89,6 @@ - diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 280bec65b..1021d8823 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net; using MediaBrowser.Common.Threading; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.EntryPoints { @@ -17,17 +18,17 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private readonly ISsdpHandler _ssdp; + private readonly IDeviceDiscovery _deviceDiscovery; private PeriodicTimer _timer; private bool _isStarted; - public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, ISsdpHandler ssdp) + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery) { _logger = logmanager.GetLogger("PortMapper"); _appHost = appHost; _config = config; - _ssdp = ssdp; + _deviceDiscovery = deviceDiscovery; } private string _lastConfigIdentifier; @@ -61,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints public void Run() { - //NatUtility.Logger = new LogWriter(_logger); + NatUtility.Logger = _logger; if (_config.Configuration.EnableUPnP) { @@ -93,33 +94,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - _ssdp.MessageReceived += _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; _lastConfigIdentifier = GetConfigIdentifier(); _isStarted = true; } - private void ClearCreatedRules(object state) + private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { - _createdRules = new List(); - _usnsHandled = new List(); - } - - void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e) - { - var endpoint = e.EndPoint as IPEndPoint; - - if (endpoint == null || e.LocalEndPoint == null) - { - return; - } + var info = e.Argument; string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; // Filter device type if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && @@ -132,15 +122,45 @@ namespace MediaBrowser.Server.Implementations.EntryPoints var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; - if (!_usnsHandled.Contains(identifier)) + if (info.Location != null && !_usnsHandled.Contains(identifier)) { _usnsHandled.Add(identifier); _logger.Debug("Calling Nat.Handle on " + identifier); - NatUtility.Handle(e.LocalEndPoint.Address, e.Message, endpoint, NatProtocol.Upnp); + + IPAddress address; + if (IPAddress.TryParse(info.Location.Host, out address)) + { + // The Handle method doesn't need the port + var endpoint = new IPEndPoint(address, info.Location.Port); + + IPAddress localAddress = null; + + try + { + var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } + } + catch + { + return; + } + + NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + } } } + private void ClearCreatedRules(object state) + { + _createdRules = new List(); + _usnsHandled = new List(); + } + void NatUtility_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var ex = e.ExceptionObject as Exception; @@ -228,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = null; } - _ssdp.MessageReceived -= _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; try { diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index c87d10ef4..80364bb55 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -164,32 +164,16 @@ namespace MediaBrowser.Server.Implementations.IO Start(); } - private bool EnableLibraryMonitor - { - get - { - switch (ConfigurationManager.Configuration.EnableLibraryMonitor) - { - case AutoOnOff.Auto: - return Environment.OSVersion.Platform == PlatformID.Win32NT; - case AutoOnOff.Enabled: - return true; - default: - return false; - } - } - } - private bool IsLibraryMonitorEnabaled(BaseItem item) { var options = LibraryManager.GetLibraryOptions(item); - if (options != null && options.SchemaVersion >= 1) + if (options != null) { return options.EnableRealtimeMonitor; } - return EnableLibraryMonitor; + return false; } public void Start() diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 1a5ebedc2..6d2f79fa0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -166,7 +166,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); if (imageIndex > -1) { - programDict[schedule.programID].images = GetProgramLogo(ApiUrl, images[imageIndex]); + var programEntry = programDict[schedule.programID]; + + var data = images[imageIndex].data ?? new List(); + data = data.OrderByDescending(GetSizeOrder).ToList(); + + programEntry.primaryImage = GetProgramImage(ApiUrl, data, "Logo", true); + //programEntry.thumbImage = GetProgramImage(ApiUrl, data, "Iconic", false); + //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LOT", false); + + if (!string.IsNullOrWhiteSpace(programEntry.thumbImage)) + { + var b = true; + } + + if (!string.IsNullOrWhiteSpace(programEntry.bannerImage)) + { + var b = true; + } } } @@ -179,6 +199,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return programsInfo; } + private int GetSizeOrder(ScheduleDirect.ImageData image) + { + if (!string.IsNullOrWhiteSpace(image.size)) + { + int value; + if (int.TryParse(image.size, out value)) + { + return value; + } + } + + return 0; + } + private readonly object _channelCacheLock = new object(); private ScheduleDirect.Station GetStation(string listingsId, string channelNumber, string channelName) { @@ -384,13 +418,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings episodeTitle = details.episodeTitle150; } - string imageUrl = null; - - if (details.hasImageArtwork) - { - imageUrl = details.images; - } - var showType = details.showType ?? string.Empty; var info = new ProgramInfo @@ -406,7 +433,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings Audio = audioType, IsRepeat = repeat, IsSeries = showType.IndexOf("series", StringComparison.OrdinalIgnoreCase) != -1, - ImageUrl = imageUrl, + ImageUrl = details.primaryImage, IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase), IsSports = showType.IndexOf("sports", StringComparison.OrdinalIgnoreCase) != -1, IsMovie = showType.IndexOf("movie", StringComparison.OrdinalIgnoreCase) != -1 || showType.IndexOf("film", StringComparison.OrdinalIgnoreCase) != -1, @@ -485,36 +512,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return date; } - private string GetProgramLogo(string apiUrl, ScheduleDirect.ShowImages images) + private string GetProgramImage(string apiUrl, List images, string category, bool returnDefaultImage) { string url = null; - if (images.data != null) - { - var smallImages = images.data.Where(i => i.size == "Sm").ToList(); - if (smallImages.Any()) - { - images.data = smallImages; - } - var logoIndex = images.data.FindIndex(i => i.category == "Logo"); - if (logoIndex == -1) - { - logoIndex = 0; - } - var uri = images.data[logoIndex].uri; - if (!string.IsNullOrWhiteSpace(uri)) + var logoIndex = images.FindIndex(i => string.Equals(i.category, category, StringComparison.OrdinalIgnoreCase)); + if (logoIndex == -1) + { + if (!returnDefaultImage) { - if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) - { - url = uri; - } - else - { - url = apiUrl + "/image/" + uri; - } + return null; } - //_logger.Debug("URL for image is : " + url); + logoIndex = 0; } + var uri = images[logoIndex].uri; + + if (!string.IsNullOrWhiteSpace(uri)) + { + if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) + { + url = uri; + } + else + { + url = apiUrl + "/image/" + uri; + } + } + //_logger.Debug("URL for image is : " + url); return url; } @@ -1204,7 +1228,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings public List crew { get; set; } public string showType { get; set; } public bool hasImageArtwork { get; set; } - public string images { get; set; } + public string primaryImage { get; set; } + public string thumbImage { get; set; } + public string bannerImage { get; set; } public string imageID { get; set; } public string md5 { get; set; } public List contentAdvisory { get; set; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs index 9ba1c60cc..ef37e3b35 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs @@ -10,6 +10,7 @@ using System; using System.Linq; using System.Threading; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -39,13 +40,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { string server = null; - if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) + var info = e.Argument; + + if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) { string location; - if (e.Headers.TryGetValue("Location", out location)) + if (info.Headers.TryGetValue("Location", out location)) { //_logger.Debug("HdHomerun found at {0}", location); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index cb0e573da..a0b8ef5f7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -14,6 +14,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Extensions; using System.Xml.Linq; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -50,18 +51,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) { + var info = e.Argument; + string st = null; string nt = null; - e.Headers.TryGetValue("ST", out st); - e.Headers.TryGetValue("NT", out nt); + info.Headers.TryGetValue("ST", out st); + info.Headers.TryGetValue("NT", out nt); if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) || string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase)) { string location; - if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) + if (info.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) { _logger.Debug("SAT IP found at {0}", location); diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 8850f3d35..e182ad6a5 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -105,9 +105,6 @@ ..\ThirdParty\UniversalDetector\UniversalDetector.dll - - ..\packages\Mono.Nat.1.2.24.0\lib\net40\Mono.Nat.dll - @@ -390,6 +387,10 @@ {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B} MediaBrowser.Model + + {d7453b88-2266-4805-b39b-2b5a2a33e1ba} + Mono.Nat + diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 746dc7f62..94522cd50 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -5,7 +5,6 @@ - diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index bcbb10174..e7acb3f50 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -1,5 +1,5 @@  - + Debug x86 @@ -10,8 +10,9 @@ MediaBrowser.Server.Mono MediaBrowser.Server.Mono MediaBrowser.Server.Mono.MainClass - v4.5 + v4.5.1 ..\ + true diff --git a/MediaBrowser.Server.Mono/app.config b/MediaBrowser.Server.Mono/app.config index b0e8558fd..e14b908ad 100644 --- a/MediaBrowser.Server.Mono/app.config +++ b/MediaBrowser.Server.Mono/app.config @@ -1,21 +1,21 @@ - + -
+
- - + + - - + + - + diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 433855ea0..e6d9b482e 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -549,7 +549,7 @@ namespace MediaBrowser.Server.Startup.Common SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager); RegisterSingleInstance(SubtitleManager); - RegisterSingleInstance(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, this, NetworkManager)); + RegisterSingleInstance(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager)); ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager, ItemRepository); RegisterSingleInstance(ChapterManager); @@ -566,8 +566,6 @@ namespace MediaBrowser.Server.Startup.Common await sharingRepo.Initialize().ConfigureAwait(false); RegisterSingleInstance(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); - RegisterSingleInstance(new SsdpHandler(LogManager.GetLogger("SsdpHandler"), ServerConfigurationManager, this)); - var activityLogRepo = await GetActivityLogRepository().ConfigureAwait(false); RegisterSingleInstance(activityLogRepo); RegisterSingleInstance(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager)); diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 778002e50..7eba89650 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -9,9 +9,10 @@ Properties MediaBrowser.Server.Startup.Common MediaBrowser.Server.Startup.Common - v4.5 + v4.5.1 512 ..\ + true diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 0b9dd90cd..7e0d834fa 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -64,6 +64,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Startup EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.Drawing", "Emby.Drawing\Emby.Drawing.csproj", "{08FFF49B-F175-4807-A2B5-73B0EBD9F716}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{D7453B88-2266-4805-B39B-2B5A2A33E1BA}" +EndProject Global GlobalSection(Performance) = preSolution HasPerformanceSessions = true @@ -522,6 +524,36 @@ Global {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Win32.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x64.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Win32.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|Win32.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x64.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Debug|x86.Build.0 = Debug|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Mixed Platforms.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Win32.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|Win32.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x64.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x64.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release Mono|x86.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Any CPU.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Win32.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|Win32.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x64.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x64.Build.0 = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x86.ActiveCfg = Release|Any CPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Mono.Nat/AbstractNatDevice.cs b/Mono.Nat/AbstractNatDevice.cs new file mode 100644 index 000000000..046cfc10f --- /dev/null +++ b/Mono.Nat/AbstractNatDevice.cs @@ -0,0 +1,97 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat +{ + public abstract class AbstractNatDevice : INatDevice + { + private DateTime lastSeen; + + protected AbstractNatDevice () + { + + } + + public abstract IPAddress LocalAddress { get; } + + public DateTime LastSeen + { + get { return lastSeen; } + set { lastSeen = value; } + } + + public virtual void CreatePortMap (Mapping mapping) + { + IAsyncResult result = BeginCreatePortMap (mapping, null, null); + EndCreatePortMap(result); + } + + public virtual void DeletePortMap (Mapping mapping) + { + IAsyncResult result = BeginDeletePortMap (mapping, null, mapping); + EndDeletePortMap(result); + } + + public virtual Mapping[] GetAllMappings () + { + IAsyncResult result = BeginGetAllMappings (null, null); + return EndGetAllMappings (result); + } + + public virtual IPAddress GetExternalIP () + { + IAsyncResult result = BeginGetExternalIP(null, null); + return EndGetExternalIP(result); + } + + public virtual Mapping GetSpecificMapping (Protocol protocol, int port) + { + IAsyncResult result = this.BeginGetSpecificMapping (protocol, port, null, null); + return this.EndGetSpecificMapping(result); + } + + public abstract IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + + public abstract IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState); + public abstract IAsyncResult BeginGetSpecificMapping(Protocol protocol, int externalPort, AsyncCallback callback, object asyncState); + + public abstract void EndCreatePortMap (IAsyncResult result); + public abstract void EndDeletePortMap (IAsyncResult result); + + public abstract Mapping[] EndGetAllMappings (IAsyncResult result); + public abstract IPAddress EndGetExternalIP (IAsyncResult result); + public abstract Mapping EndGetSpecificMapping (IAsyncResult result); + } +} diff --git a/Mono.Nat/AsyncResults/AsyncResult.cs b/Mono.Nat/AsyncResults/AsyncResult.cs new file mode 100644 index 000000000..e98e7d7ca --- /dev/null +++ b/Mono.Nat/AsyncResults/AsyncResult.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Mono.Nat +{ + internal class AsyncResult : IAsyncResult + { + private object asyncState; + private AsyncCallback callback; + private bool completedSynchronously; + private bool isCompleted; + private Exception storedException; + private ManualResetEvent waitHandle; + + public AsyncResult(AsyncCallback callback, object asyncState) + { + this.callback = callback; + this.asyncState = asyncState; + waitHandle = new ManualResetEvent(false); + } + + public object AsyncState + { + get { return asyncState; } + } + + public ManualResetEvent AsyncWaitHandle + { + get { return waitHandle; } + } + + WaitHandle IAsyncResult.AsyncWaitHandle + { + get { return waitHandle; } + } + + public bool CompletedSynchronously + { + get { return completedSynchronously; } + protected internal set { completedSynchronously = value; } + } + + public bool IsCompleted + { + get { return isCompleted; } + protected internal set { isCompleted = value; } + } + + public Exception StoredException + { + get { return storedException; } + } + + public void Complete() + { + Complete(storedException); + } + + public void Complete(Exception ex) + { + storedException = ex; + isCompleted = true; + waitHandle.Set(); + + if (callback != null) + callback(this); + } + } +} diff --git a/Mono.Nat/Enums/MapState.cs b/Mono.Nat/Enums/MapState.cs new file mode 100644 index 000000000..5ed2abd8f --- /dev/null +++ b/Mono.Nat/Enums/MapState.cs @@ -0,0 +1,36 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public enum MapState + { + AlreadyMapped, + Available + } +} \ No newline at end of file diff --git a/Mono.Nat/Enums/ProtocolType.cs b/Mono.Nat/Enums/ProtocolType.cs new file mode 100644 index 000000000..a1f5cbb0e --- /dev/null +++ b/Mono.Nat/Enums/ProtocolType.cs @@ -0,0 +1,36 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public enum Protocol + { + Tcp, + Udp + } +} \ No newline at end of file diff --git a/Mono.Nat/EventArgs/DeviceEventArgs.cs b/Mono.Nat/EventArgs/DeviceEventArgs.cs new file mode 100644 index 000000000..fbbbf63e3 --- /dev/null +++ b/Mono.Nat/EventArgs/DeviceEventArgs.cs @@ -0,0 +1,45 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public class DeviceEventArgs : EventArgs + { + private INatDevice device; + + public DeviceEventArgs(INatDevice device) + { + this.device = device; + } + + public INatDevice Device + { + get { return this.device; } + } + } +} \ No newline at end of file diff --git a/Mono.Nat/Exceptions/MappingException.cs b/Mono.Nat/Exceptions/MappingException.cs new file mode 100644 index 000000000..bb2e6a69d --- /dev/null +++ b/Mono.Nat/Exceptions/MappingException.cs @@ -0,0 +1,87 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Security.Permissions; + +namespace Mono.Nat +{ + [Serializable] + public class MappingException : Exception + { + private int errorCode; + private string errorText; + + public int ErrorCode + { + get { return this.errorCode; } + } + + public string ErrorText + { + get { return this.errorText; } + } + + #region Constructors + public MappingException() + : base() + { + } + + public MappingException(string message) + : base(message) + { + } + + public MappingException(int errorCode, string errorText) + : base (string.Format ("Error {0}: {1}", errorCode, errorText)) + { + this.errorCode = errorCode; + this.errorText = errorText; + } + + public MappingException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected MappingException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + } + #endregion + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter=true)] + public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + { + if(info==null) throw new ArgumentNullException("info"); + + this.errorCode = info.GetInt32("errorCode"); + this.errorText = info.GetString("errorText"); + base.GetObjectData(info, context); + } + } +} diff --git a/Mono.Nat/IMapper.cs b/Mono.Nat/IMapper.cs new file mode 100644 index 000000000..b18e6cff2 --- /dev/null +++ b/Mono.Nat/IMapper.cs @@ -0,0 +1,50 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Nat +{ + public enum MapperType + { + Pmp, + Upnp + } + + internal interface IMapper + { + event EventHandler DeviceFound; + + void Map(IPAddress gatewayAddress); + + void Handle(IPAddress localAddres, byte[] response); + } +} diff --git a/Mono.Nat/INatDevice.cs b/Mono.Nat/INatDevice.cs new file mode 100644 index 000000000..c9f27055b --- /dev/null +++ b/Mono.Nat/INatDevice.cs @@ -0,0 +1,62 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat +{ + public interface INatDevice + { + void CreatePortMap (Mapping mapping); + void DeletePortMap (Mapping mapping); + + IPAddress LocalAddress { get; } + Mapping[] GetAllMappings (); + IPAddress GetExternalIP (); + Mapping GetSpecificMapping (Protocol protocol, int port); + + IAsyncResult BeginCreatePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState); + + IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState); + IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState); + IAsyncResult BeginGetSpecificMapping (Protocol protocol, int externalPort, AsyncCallback callback, object asyncState); + + void EndCreatePortMap (IAsyncResult result); + void EndDeletePortMap (IAsyncResult result); + + Mapping[] EndGetAllMappings (IAsyncResult result); + IPAddress EndGetExternalIP (IAsyncResult result); + Mapping EndGetSpecificMapping (IAsyncResult result); + + DateTime LastSeen { get; set; } + } +} diff --git a/Mono.Nat/ISearcher.cs b/Mono.Nat/ISearcher.cs new file mode 100644 index 000000000..56e438105 --- /dev/null +++ b/Mono.Nat/ISearcher.cs @@ -0,0 +1,51 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net.Sockets; +using System.Net; + +namespace Mono.Nat +{ + public delegate void NatDeviceCallback(INatDevice device); + + internal interface ISearcher + { + event EventHandler DeviceFound; + event EventHandler DeviceLost; + + void Search(); + void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint); + DateTime NextSearch { get; } + NatProtocol Protocol { get; } + } +} diff --git a/Mono.Nat/Mapping.cs b/Mono.Nat/Mapping.cs new file mode 100644 index 000000000..dd49404c6 --- /dev/null +++ b/Mono.Nat/Mapping.cs @@ -0,0 +1,123 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat +{ + public class Mapping + { + private string description; + private DateTime expiration; + private int lifetime; + private int privatePort; + private Protocol protocol; + private int publicPort; + + + + public Mapping (Protocol protocol, int privatePort, int publicPort) + : this (protocol, privatePort, publicPort, 0) + { + } + + public Mapping (Protocol protocol, int privatePort, int publicPort, int lifetime) + { + this.protocol = protocol; + this.privatePort = privatePort; + this.publicPort = publicPort; + this.lifetime = lifetime; + + if (lifetime == int.MaxValue) + this.expiration = DateTime.MaxValue; + else if (lifetime == 0) + this.expiration = DateTime.Now; + else + this.expiration = DateTime.Now.AddSeconds (lifetime); + } + + public string Description + { + get { return description; } + set { description = value; } + } + + public Protocol Protocol + { + get { return protocol; } + internal set { protocol = value; } + } + + public int PrivatePort + { + get { return privatePort; } + internal set { privatePort = value; } + } + + public int PublicPort + { + get { return publicPort; } + internal set { publicPort = value; } + } + + public int Lifetime + { + get { return lifetime; } + internal set { lifetime = value; } + } + + public DateTime Expiration + { + get { return expiration; } + internal set { expiration = value; } + } + + public bool IsExpired () + { + return expiration < DateTime.Now; + } + + public override bool Equals (object obj) + { + Mapping other = obj as Mapping; + return other == null ? false : this.protocol == other.protocol && + this.privatePort == other.privatePort && this.publicPort == other.publicPort; + } + + public override int GetHashCode() + { + return this.protocol.GetHashCode() ^ this.privatePort.GetHashCode() ^ this.publicPort.GetHashCode(); + } + + public override string ToString( ) + { + return String.Format( "Protocol: {0}, Public Port: {1}, Private Port: {2}, Description: {3}, Expiration: {4}, Lifetime: {5}", + this.protocol, this.publicPort, this.privatePort, this.description, this.expiration, this.lifetime ); + } + } +} diff --git a/Mono.Nat/Mono.Nat.csproj b/Mono.Nat/Mono.Nat.csproj new file mode 100644 index 000000000..9c2781433 --- /dev/null +++ b/Mono.Nat/Mono.Nat.csproj @@ -0,0 +1,104 @@ + + + + + Debug + AnyCPU + {D7453B88-2266-4805-B39B-2B5A2A33E1BA} + Library + Properties + Mono.Nat + Mono.Nat + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/Mono.Nat/NatProtocol.cs b/Mono.Nat/NatProtocol.cs new file mode 100644 index 000000000..ade8d921c --- /dev/null +++ b/Mono.Nat/NatProtocol.cs @@ -0,0 +1,9 @@ + +namespace Mono.Nat +{ + public enum NatProtocol + { + Upnp = 0, + Pmp = 1 + } +} diff --git a/Mono.Nat/NatUtility.cs b/Mono.Nat/NatUtility.cs new file mode 100644 index 000000000..6d91d2513 --- /dev/null +++ b/Mono.Nat/NatUtility.cs @@ -0,0 +1,264 @@ +// +// Authors: +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Net.NetworkInformation; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Logging; +using Mono.Nat.Pmp.Mappers; +using Mono.Nat.Upnp.Mappers; + +namespace Mono.Nat +{ + public static class NatUtility + { + private static ManualResetEvent searching; + public static event EventHandler DeviceFound; + public static event EventHandler DeviceLost; + + public static event EventHandler UnhandledException; + + private static List controllers; + private static bool verbose; + + public static List EnabledProtocols { get; set; } + + public static ILogger Logger { get; set; } + + public static bool Verbose + { + get { return verbose; } + set { verbose = value; } + } + + static NatUtility() + { + EnabledProtocols = new List + { + NatProtocol.Upnp, + NatProtocol.Pmp + }; + + searching = new ManualResetEvent(false); + + controllers = new List(); + controllers.Add(UpnpSearcher.Instance); + controllers.Add(PmpSearcher.Instance); + + controllers.ForEach(searcher => + { + searcher.DeviceFound += (sender, args) => + { + if (DeviceFound != null) + DeviceFound(sender, args); + }; + searcher.DeviceLost += (sender, args) => + { + if (DeviceLost != null) + DeviceLost(sender, args); + }; + }); + Thread t = new Thread(SearchAndListen); + t.IsBackground = true; + t.Start(); + } + + internal static void Log(string format, params object[] args) + { + var logger = Logger; + if (logger != null) + logger.Debug(format, args); + } + + private static void SearchAndListen() + { + while (true) + { + searching.WaitOne(); + + try + { + var enabledProtocols = EnabledProtocols.ToList(); + + if (enabledProtocols.Contains(UpnpSearcher.Instance.Protocol)) + { + Receive(UpnpSearcher.Instance, UpnpSearcher.sockets); + } + if (enabledProtocols.Contains(PmpSearcher.Instance.Protocol)) + { + Receive(PmpSearcher.Instance, PmpSearcher.sockets); + } + + foreach (ISearcher s in controllers) + if (s.NextSearch < DateTime.Now && enabledProtocols.Contains(s.Protocol)) + { + Log("Searching for: {0}", s.GetType().Name); + s.Search(); + } + } + catch (Exception e) + { + if (UnhandledException != null) + UnhandledException(typeof(NatUtility), new UnhandledExceptionEventArgs(e, false)); + } + Thread.Sleep(10); + } + } + + static void Receive (ISearcher searcher, List clients) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + foreach (UdpClient client in clients) + { + if (client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)client.Client.LocalEndPoint).Address; + byte[] data = client.Receive(ref received); + searcher.Handle(localAddress, data, received); + } + } + } + + static void Receive(IMapper mapper, List clients) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + foreach (UdpClient client in clients) + { + if (client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)client.Client.LocalEndPoint).Address; + byte[] data = client.Receive(ref received); + mapper.Handle(localAddress, data); + } + } + } + + public static void StartDiscovery () + { + searching.Set(); + } + + public static void StopDiscovery () + { + searching.Reset(); + } + + //This is for when you know the Gateway IP and want to skip the costly search... + public static void DirectMap(IPAddress gatewayAddress, MapperType type) + { + IMapper mapper; + switch (type) + { + case MapperType.Pmp: + mapper = new PmpMapper(); + break; + case MapperType.Upnp: + mapper = new UpnpMapper(); + mapper.DeviceFound += (sender, args) => + { + if (DeviceFound != null) + DeviceFound(sender, args); + }; + mapper.Map(gatewayAddress); + break; + default: + throw new InvalidOperationException("Unsuported type given"); + + } + searching.Reset(); + + } + + //So then why is it here? -Nick + [Obsolete ("This method serves no purpose and shouldn't be used")] + public static IPAddress[] GetLocalAddresses (bool includeIPv6) + { + List addresses = new List (); + + IPHostEntry hostInfo = Dns.GetHostEntry (Dns.GetHostName ()); + foreach (IPAddress address in hostInfo.AddressList) { + if (address.AddressFamily == AddressFamily.InterNetwork || + (includeIPv6 && address.AddressFamily == AddressFamily.InterNetworkV6)) { + addresses.Add (address); + } + } + + return addresses.ToArray (); + } + + //checks if an IP address is a private address space as defined by RFC 1918 + public static bool IsPrivateAddressSpace (IPAddress address) + { + byte[] ba = address.GetAddressBytes (); + + switch ((int)ba[0]) { + case 10: + return true; //10.x.x.x + case 172: + return ((int)ba[1] & 16) != 0; //172.16-31.x.x + case 192: + return (int)ba[1] == 168; //192.168.x.x + default: + return false; + } + } + + public static void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint, NatProtocol protocol) + { + switch (protocol) + { + case NatProtocol.Upnp: + UpnpSearcher.Instance.Handle(localAddress, response, endpoint); + break; + case NatProtocol.Pmp: + PmpSearcher.Instance.Handle(localAddress, response, endpoint); + break; + default: + throw new ArgumentException("Unexpected protocol: " + protocol); + } + } + + public static void Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint, NatProtocol protocol) + { + switch (protocol) + { + case NatProtocol.Upnp: + UpnpSearcher.Instance.Handle(localAddress, deviceInfo, endpoint); + break; + default: + throw new ArgumentException("Unexpected protocol: " + protocol); + } + } + } +} diff --git a/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs b/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs new file mode 100644 index 000000000..c8ccf5435 --- /dev/null +++ b/Mono.Nat/Pmp/AsyncResults/PortMapAsyncResult.cs @@ -0,0 +1,52 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Pmp +{ + internal class PortMapAsyncResult : AsyncResult + { + private Mapping mapping; + + internal PortMapAsyncResult (Mapping mapping, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.mapping = mapping; + } + + internal PortMapAsyncResult (Protocol protocol, int port, int lifetime, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.mapping = new Mapping (protocol, port, port, lifetime); + } + + internal Mapping Mapping + { + get { return mapping; } + } + } +} diff --git a/Mono.Nat/Pmp/Mappers/PmpMapper.cs b/Mono.Nat/Pmp/Mappers/PmpMapper.cs new file mode 100644 index 000000000..f33ca44c3 --- /dev/null +++ b/Mono.Nat/Pmp/Mappers/PmpMapper.cs @@ -0,0 +1,83 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Mono.Nat.Pmp; + +namespace Mono.Nat.Pmp.Mappers +{ + internal class PmpMapper : Pmp, IMapper + { + public event EventHandler DeviceFound; + + static PmpMapper() + { + CreateSocketsAndAddGateways(); + } + + public void Map(IPAddress gatewayAddress) + { + sockets.ForEach(x => Map(x, gatewayAddress)); + } + + void Map(UdpClient client, IPAddress gatewayAddress) + { + // The nat-pmp search message. Must be sent to GatewayIP:53531 + byte[] buffer = new byte[] { PmpConstants.Version, PmpConstants.OperationCode }; + + client.Send(buffer, buffer.Length, new IPEndPoint(gatewayAddress, PmpConstants.ServerPort)); + } + + public void Handle(IPAddress localAddres, byte[] response) + { + //if (!IsSearchAddress(endpoint.Address)) + // return; + if (response.Length != 12) + return; + if (response[0] != PmpConstants.Version) + return; + if (response[1] != PmpConstants.ServerNoop) + return; + int errorcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(response, 2)); + if (errorcode != 0) + NatUtility.Log("Non zero error: {0}", errorcode); + + IPAddress publicIp = new IPAddress(new byte[] { response[8], response[9], response[10], response[11] }); + OnDeviceFound(new DeviceEventArgs(new PmpNatDevice(localAddres, publicIp))); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + } +} diff --git a/Mono.Nat/Pmp/Pmp.cs b/Mono.Nat/Pmp/Pmp.cs new file mode 100644 index 000000000..6795561b1 --- /dev/null +++ b/Mono.Nat/Pmp/Pmp.cs @@ -0,0 +1,118 @@ +// +// Authors: +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Nat.Pmp +{ + internal abstract class Pmp + { + public static List sockets; + protected static Dictionary> gatewayLists; + + internal static void CreateSocketsAndAddGateways() + { + sockets = new List(); + gatewayLists = new Dictionary>(); + + try + { + foreach (NetworkInterface n in NetworkInterface.GetAllNetworkInterfaces()) + { + if (n.OperationalStatus != OperationalStatus.Up && n.OperationalStatus != OperationalStatus.Unknown) + continue; + IPInterfaceProperties properties = n.GetIPProperties(); + List gatewayList = new List(); + + foreach (GatewayIPAddressInformation gateway in properties.GatewayAddresses) + { + if (gateway.Address.AddressFamily == AddressFamily.InterNetwork) + { + gatewayList.Add(new IPEndPoint(gateway.Address, PmpConstants.ServerPort)); + } + } + if (gatewayList.Count == 0) + { + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + foreach (var gw2 in properties.DnsAddresses) + { + if (gw2.AddressFamily == AddressFamily.InterNetwork) + { + gatewayList.Add(new IPEndPoint(gw2, PmpConstants.ServerPort)); + } + } + foreach (var unicast in properties.UnicastAddresses) + { + if (/*unicast.DuplicateAddressDetectionState == DuplicateAddressDetectionState.Preferred + && unicast.AddressPreferredLifetime != UInt32.MaxValue + && */unicast.Address.AddressFamily == AddressFamily.InterNetwork) + { + var bytes = unicast.Address.GetAddressBytes(); + bytes[3] = 1; + gatewayList.Add(new IPEndPoint(new IPAddress(bytes), PmpConstants.ServerPort)); + } + } + } + + if (gatewayList.Count > 0) + { + foreach (UnicastIPAddressInformation address in properties.UnicastAddresses) + { + if (address.Address.AddressFamily == AddressFamily.InterNetwork) + { + UdpClient client; + + try + { + client = new UdpClient(new IPEndPoint(address.Address, 0)); + } + catch (SocketException) + { + continue; // Move on to the next address. + } + + gatewayLists.Add(client, gatewayList); + sockets.Add(client); + } + } + } + } + } + catch (Exception) + { + // NAT-PMP does not use multicast, so there isn't really a good fallback. + } + } + } +} diff --git a/Mono.Nat/Pmp/PmpConstants.cs b/Mono.Nat/Pmp/PmpConstants.cs new file mode 100644 index 000000000..ff3eb6230 --- /dev/null +++ b/Mono.Nat/Pmp/PmpConstants.cs @@ -0,0 +1,56 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Pmp +{ + internal static class PmpConstants + { + public const byte Version = (byte)0; + + public const byte OperationCode = (byte)0; + public const byte OperationCodeUdp = (byte)1; + public const byte OperationCodeTcp = (byte)2; + public const byte ServerNoop = (byte)128; + + public const int ClientPort = 5350; + public const int ServerPort = 5351; + + public const int RetryDelay = 250; + public const int RetryAttempts = 9; + + public const int RecommendedLeaseTime = 60 * 60; + public const int DefaultLeaseTime = RecommendedLeaseTime; + + public const short ResultCodeSuccess = 0; + public const short ResultCodeUnsupportedVersion = 1; + public const short ResultCodeNotAuthorized = 2; + public const short ResultCodeNetworkFailure = 3; + public const short ResultCodeOutOfResources = 4; + public const short ResultCodeUnsupportedOperationCode = 5; + } +} \ No newline at end of file diff --git a/Mono.Nat/Pmp/PmpNatDevice.cs b/Mono.Nat/Pmp/PmpNatDevice.cs new file mode 100644 index 000000000..9a2962c4d --- /dev/null +++ b/Mono.Nat/Pmp/PmpNatDevice.cs @@ -0,0 +1,347 @@ +// +// Authors: +// Ben Motmans +// +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Collections.Generic; + +namespace Mono.Nat.Pmp +{ + internal sealed class PmpNatDevice : AbstractNatDevice, IEquatable + { + private AsyncResult externalIpResult; + private bool pendingOp; + private IPAddress localAddress; + private IPAddress publicAddress; + + internal PmpNatDevice (IPAddress localAddress, IPAddress publicAddress) + { + this.localAddress = localAddress; + this.publicAddress = publicAddress; + } + + public override IPAddress LocalAddress + { + get { return localAddress; } + } + + public override IPAddress GetExternalIP () + { + return publicAddress; + } + + public override IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + PortMapAsyncResult pmar = new PortMapAsyncResult (mapping.Protocol, mapping.PublicPort, PmpConstants.DefaultLeaseTime, callback, asyncState); + ThreadPool.QueueUserWorkItem (delegate + { + try + { + CreatePortMap(pmar.Mapping, true); + pmar.Complete(); + } + catch (Exception e) + { + pmar.Complete(e); + } + }); + return pmar; + } + + public override IAsyncResult BeginDeletePortMap (Mapping mapping, AsyncCallback callback, object asyncState) + { + PortMapAsyncResult pmar = new PortMapAsyncResult (mapping, callback, asyncState); + ThreadPool.QueueUserWorkItem (delegate { + try { + CreatePortMap(pmar.Mapping, false); + pmar.Complete(); + } catch (Exception e) { + pmar.Complete(e); + } + }); + return pmar; + } + + public override void EndCreatePortMap (IAsyncResult result) + { + PortMapAsyncResult pmar = result as PortMapAsyncResult; + pmar.AsyncWaitHandle.WaitOne (); + } + + public override void EndDeletePortMap (IAsyncResult result) + { + PortMapAsyncResult pmar = result as PortMapAsyncResult; + pmar.AsyncWaitHandle.WaitOne (); + } + + public override IAsyncResult BeginGetAllMappings (AsyncCallback callback, object asyncState) + { + //NAT-PMP does not specify a way to get all port mappings + throw new NotSupportedException (); + } + + public override IAsyncResult BeginGetExternalIP (AsyncCallback callback, object asyncState) + { + StartOp(ref externalIpResult, callback, asyncState); + AsyncResult result = externalIpResult; + result.Complete(); + return result; + } + + public override IAsyncResult BeginGetSpecificMapping (Protocol protocol, int port, AsyncCallback callback, object asyncState) + { + //NAT-PMP does not specify a way to get a specific port map + throw new NotSupportedException (); + } + + public override Mapping[] EndGetAllMappings (IAsyncResult result) + { + //NAT-PMP does not specify a way to get all port mappings + throw new NotSupportedException (); + } + + public override IPAddress EndGetExternalIP (IAsyncResult result) + { + EndOp(result, ref externalIpResult); + return publicAddress; + } + + private void StartOp(ref AsyncResult result, AsyncCallback callback, object asyncState) + { + if (pendingOp == true) + throw new InvalidOperationException("Can only have one simultaenous async operation"); + + pendingOp = true; + result = new AsyncResult(callback, asyncState); + } + + private void EndOp(IAsyncResult supplied, ref AsyncResult actual) + { + if (supplied == null) + throw new ArgumentNullException("result"); + + if (supplied != actual) + throw new ArgumentException("Supplied IAsyncResult does not match the stored result"); + + if (!supplied.IsCompleted) + supplied.AsyncWaitHandle.WaitOne(); + + if (actual.StoredException != null) + throw actual.StoredException; + + pendingOp = false; + actual = null; + } + + public override Mapping EndGetSpecificMapping (IAsyncResult result) + { + //NAT-PMP does not specify a way to get a specific port map + throw new NotSupportedException (); + } + + public override bool Equals(object obj) + { + PmpNatDevice device = obj as PmpNatDevice; + return (device == null) ? false : this.Equals(device); + } + + public override int GetHashCode () + { + return this.publicAddress.GetHashCode(); + } + + public bool Equals (PmpNatDevice other) + { + return (other == null) ? false : this.publicAddress.Equals(other.publicAddress); + } + + private Mapping CreatePortMap (Mapping mapping, bool create) + { + List package = new List (); + + package.Add (PmpConstants.Version); + package.Add (mapping.Protocol == Protocol.Tcp ? PmpConstants.OperationCodeTcp : PmpConstants.OperationCodeUdp); + package.Add ((byte)0); //reserved + package.Add ((byte)0); //reserved + package.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder((short)mapping.PrivatePort))); + package.AddRange (BitConverter.GetBytes (create ? IPAddress.HostToNetworkOrder((short)mapping.PublicPort) : (short)0)); + package.AddRange (BitConverter.GetBytes (IPAddress.HostToNetworkOrder(mapping.Lifetime))); + + CreatePortMapAsyncState state = new CreatePortMapAsyncState (); + state.Buffer = package.ToArray (); + state.Mapping = mapping; + + ThreadPool.QueueUserWorkItem (new WaitCallback (CreatePortMapAsync), state); + WaitHandle.WaitAll (new WaitHandle[] {state.ResetEvent}); + + if (!state.Success) { + string type = create ? "create" : "delete"; + throw new MappingException (String.Format ("Failed to {0} portmap (protocol={1}, private port={2}", type, mapping.Protocol, mapping.PrivatePort)); + } + + return state.Mapping; + } + + private void CreatePortMapAsync (object obj) + { + CreatePortMapAsyncState state = obj as CreatePortMapAsyncState; + + UdpClient udpClient = new UdpClient (); + CreatePortMapListenState listenState = new CreatePortMapListenState (state, udpClient); + + int attempt = 0; + int delay = PmpConstants.RetryDelay; + + ThreadPool.QueueUserWorkItem (new WaitCallback (CreatePortMapListen), listenState); + + while (attempt < PmpConstants.RetryAttempts && !listenState.Success) { + udpClient.Send (state.Buffer, state.Buffer.Length, new IPEndPoint (localAddress, PmpConstants.ServerPort)); + listenState.UdpClientReady.Set(); + + attempt++; + delay *= 2; + Thread.Sleep (delay); + } + + state.Success = listenState.Success; + + udpClient.Close (); + state.ResetEvent.Set (); + } + + private void CreatePortMapListen (object obj) + { + CreatePortMapListenState state = obj as CreatePortMapListenState; + + UdpClient udpClient = state.UdpClient; + state.UdpClientReady.WaitOne(); // Evidently UdpClient has some lazy-init Send/Receive race? + IPEndPoint endPoint = new IPEndPoint (localAddress, PmpConstants.ServerPort); + + while (!state.Success) + { + byte[] data; + try + { + data = udpClient.Receive(ref endPoint); + } + catch (SocketException) + { + state.Success = false; + return; + } + + catch (ObjectDisposedException) + { + state.Success = false; + return; + } + + if (data.Length < 16) + continue; + + if (data[0] != PmpConstants.Version) + continue; + + byte opCode = (byte)(data[1] & (byte)127); + + Protocol protocol = Protocol.Tcp; + if (opCode == PmpConstants.OperationCodeUdp) + protocol = Protocol.Udp; + + short resultCode = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 2)); + uint epoch = (uint)IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (data, 4)); + + int privatePort = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 8)); + int publicPort = IPAddress.NetworkToHostOrder (BitConverter.ToInt16 (data, 10)); + + uint lifetime = (uint)IPAddress.NetworkToHostOrder (BitConverter.ToInt32 (data, 12)); + + if (publicPort < 0 || privatePort < 0 || resultCode != PmpConstants.ResultCodeSuccess) + { + state.Success = false; + return; + } + + if (lifetime == 0) + { + //mapping was deleted + state.Success = true; + state.Mapping = null; + return; + } + else + { + //mapping was created + //TODO: verify that the private port+protocol are a match + Mapping mapping = state.Mapping; + mapping.PublicPort = publicPort; + mapping.Protocol = protocol; + mapping.Expiration = DateTime.Now.AddSeconds (lifetime); + + state.Success = true; + } + } + } + + + /// + /// Overridden. + /// + /// + public override string ToString( ) + { + return String.Format( "PmpNatDevice - Local Address: {0}, Public IP: {1}, Last Seen: {2}", + this.localAddress, this.publicAddress, this.LastSeen ); + } + + + private class CreatePortMapAsyncState + { + internal byte[] Buffer; + internal ManualResetEvent ResetEvent = new ManualResetEvent (false); + internal Mapping Mapping; + + internal bool Success; + } + + private class CreatePortMapListenState + { + internal volatile bool Success; + internal Mapping Mapping; + internal UdpClient UdpClient; + internal ManualResetEvent UdpClientReady; + + internal CreatePortMapListenState (CreatePortMapAsyncState state, UdpClient client) + { + Mapping = state.Mapping; + UdpClient = client; UdpClientReady = new ManualResetEvent(false); + } + } + } +} \ No newline at end of file diff --git a/Mono.Nat/Pmp/Searchers/PmpSearcher.cs b/Mono.Nat/Pmp/Searchers/PmpSearcher.cs new file mode 100644 index 000000000..df0273ccb --- /dev/null +++ b/Mono.Nat/Pmp/Searchers/PmpSearcher.cs @@ -0,0 +1,149 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using Mono.Nat.Pmp; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Linq; + +namespace Mono.Nat +{ + internal class PmpSearcher : Pmp.Pmp, ISearcher + { + static PmpSearcher instance = new PmpSearcher(); + + + public static PmpSearcher Instance + { + get { return instance; } + } + + private int timeout; + private DateTime nextSearch; + public event EventHandler DeviceFound; + public event EventHandler DeviceLost; + + static PmpSearcher() + { + CreateSocketsAndAddGateways(); + } + + PmpSearcher() + { + timeout = 250; + } + + public void Search() + { + foreach (UdpClient s in sockets) + { + try + { + Search(s); + } + catch + { + // Ignore any search errors + } + } + } + + void Search (UdpClient client) + { + // Sort out the time for the next search first. The spec says the + // timeout should double after each attempt. Once it reaches 64 seconds + // (and that attempt fails), assume no devices available + nextSearch = DateTime.Now.AddMilliseconds(timeout); + timeout *= 2; + + // We've tried 9 times as per spec, try searching again in 5 minutes + if (timeout == 128 * 1000) + { + timeout = 250; + nextSearch = DateTime.Now.AddMinutes(10); + return; + } + + // The nat-pmp search message. Must be sent to GatewayIP:53531 + byte[] buffer = new byte[] { PmpConstants.Version, PmpConstants.OperationCode }; + foreach (IPEndPoint gatewayEndpoint in gatewayLists[client]) + client.Send(buffer, buffer.Length, gatewayEndpoint); + } + + bool IsSearchAddress(IPAddress address) + { + foreach (List gatewayList in gatewayLists.Values) + foreach (IPEndPoint gatewayEndpoint in gatewayList) + if (gatewayEndpoint.Address.Equals(address)) + return true; + return false; + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + if (!IsSearchAddress(endpoint.Address)) + return; + if (response.Length != 12) + return; + if (response[0] != PmpConstants.Version) + return; + if (response[1] != PmpConstants.ServerNoop) + return; + int errorcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(response, 2)); + if (errorcode != 0) + NatUtility.Log("Non zero error: {0}", errorcode); + + IPAddress publicIp = new IPAddress(new byte[] { response[8], response[9], response[10], response[11] }); + nextSearch = DateTime.Now.AddMinutes(5); + timeout = 250; + OnDeviceFound(new DeviceEventArgs(new PmpNatDevice(endpoint.Address, publicIp))); + } + + public DateTime NextSearch + { + get { return nextSearch; } + } + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + + public NatProtocol Protocol + { + get { return NatProtocol.Pmp; } + } + } +} diff --git a/Mono.Nat/Properties/AssemblyInfo.cs b/Mono.Nat/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c3c3101de --- /dev/null +++ b/Mono.Nat/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Mono.Nat")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Mono.Nat")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d7453b88-2266-4805-b39b-2b5a2a33e1ba")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// \ No newline at end of file diff --git a/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs b/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs new file mode 100644 index 000000000..51ecfbaf0 --- /dev/null +++ b/Mono.Nat/Upnp/AsyncResults/GetAllMappingsAsyncResult.cs @@ -0,0 +1,56 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetAllMappingsAsyncResult : PortMapAsyncResult + { + private List mappings; + private Mapping specificMapping; + + public GetAllMappingsAsyncResult(WebRequest request, AsyncCallback callback, object asyncState) + : base(request, callback, asyncState) + { + mappings = new List(); + } + + public List Mappings + { + get { return this.mappings; } + } + + public Mapping SpecificMapping + { + get { return this.specificMapping; } + set { this.specificMapping = value; } + } + } +} diff --git a/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs b/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs new file mode 100644 index 000000000..d8ac3fe61 --- /dev/null +++ b/Mono.Nat/Upnp/AsyncResults/PortMapAsyncResult.cs @@ -0,0 +1,75 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +using System.Net; +using System.Threading; + +namespace Mono.Nat.Upnp +{ + internal class PortMapAsyncResult : AsyncResult + { + private WebRequest request; + private MessageBase savedMessage; + + protected PortMapAsyncResult(WebRequest request, AsyncCallback callback, object asyncState) + : base (callback, asyncState) + { + this.request = request; + } + + internal WebRequest Request + { + get { return this.request; } + set { this.request = value; } + } + + internal MessageBase SavedMessage + { + get { return this.savedMessage; } + set { this.savedMessage = value; } + } + + internal static PortMapAsyncResult Create (MessageBase message, WebRequest request, AsyncCallback storedCallback, object asyncState) + { + if (message is GetGenericPortMappingEntry) + return new GetAllMappingsAsyncResult(request, storedCallback, asyncState); + + if (message is GetSpecificPortMappingEntryMessage) + { + GetSpecificPortMappingEntryMessage mapMessage = (GetSpecificPortMappingEntryMessage)message; + GetAllMappingsAsyncResult result = new GetAllMappingsAsyncResult(request, storedCallback, asyncState); + + result.SpecificMapping = new Mapping(mapMessage.protocol, 0, mapMessage.externalPort, 0); + return result; + } + + return new PortMapAsyncResult(request, storedCallback, asyncState); + } + } +} diff --git a/Mono.Nat/Upnp/Mappers/UpnpMapper.cs b/Mono.Nat/Upnp/Mappers/UpnpMapper.cs new file mode 100644 index 000000000..6f2716805 --- /dev/null +++ b/Mono.Nat/Upnp/Mappers/UpnpMapper.cs @@ -0,0 +1,110 @@ +// +// Authors: +// Nicholas Terry +// +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Mono.Nat.Upnp.Mappers +{ + internal class UpnpMapper : Upnp, IMapper + { + + public event EventHandler DeviceFound; + + public UdpClient Client { get; set; } + + public UpnpMapper() + { + //Bind to local port 1900 for ssdp responses + Client = new UdpClient(1900); + } + + public void Map(IPAddress gatewayAddress) + { + //Get the httpu request payload + byte[] data = DiscoverDeviceMessage.EncodeUnicast(gatewayAddress); + + Client.Send(data, data.Length, new IPEndPoint(gatewayAddress, 1900)); + + new Thread(Receive).Start(); + } + + public void Receive() + { + while (true) + { + IPEndPoint received = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5351); + if (Client.Available > 0) + { + IPAddress localAddress = ((IPEndPoint)Client.Client.LocalEndPoint).Address; + byte[] data = Client.Receive(ref received); + Handle(localAddress, data, received); + } + } + } + + public void Handle(IPAddress localAddres, byte[] response) + { + Handle(localAddres, response, null); + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try + { + UpnpNatDevice d = base.Handle(localAddress, response, endpoint); + d.GetServicesList(DeviceSetupComplete); + } + catch (Exception ex) + { + Trace.WriteLine("Unhandled exception when trying to decode a device's response Send me the following data: "); + Trace.WriteLine("ErrorMessage:"); + Trace.WriteLine(ex.Message); + Trace.WriteLine("Data string:"); + Trace.WriteLine(Encoding.UTF8.GetString(response)); + } + } + + private void DeviceSetupComplete(INatDevice device) + { + OnDeviceFound(new DeviceEventArgs(device)); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs b/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs new file mode 100644 index 000000000..87f5835a6 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/DiscoverDeviceMessage.cs @@ -0,0 +1,60 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.Text; + +namespace Mono.Nat.Upnp +{ + internal static class DiscoverDeviceMessage + { + /// + /// The message sent to discover all uPnP devices on the network + /// + /// + public static byte[] EncodeSSDP() + { + string s = "M-SEARCH * HTTP/1.1\r\n" + + "HOST: 239.255.255.250:1900\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "MX: 3\r\n" + + "ST: ssdp:all\r\n\r\n"; + return UTF8Encoding.ASCII.GetBytes(s); + } + + public static byte[] EncodeUnicast(IPAddress gatewayAddress) + { + //Format obtained from http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf pg 31 + //This method only works with upnp 1.1 routers... unfortunately + string s = "M-SEARCH * HTTP/1.1\r\n" + + "HOST: " + gatewayAddress + ":1900\r\n" + + "MAN: \"ssdp:discover\"\r\n" + + "ST: ssdp:all\r\n\r\n"; + //+ "USER-AGENT: unix/5.1 UPnP/1.1 MyProduct/1.0\r\n\r\n"; + return UTF8Encoding.ASCII.GetBytes(s); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/ErrorMessage.cs b/Mono.Nat/Upnp/Messages/ErrorMessage.cs new file mode 100644 index 000000000..ce5270e9b --- /dev/null +++ b/Mono.Nat/Upnp/Messages/ErrorMessage.cs @@ -0,0 +1,63 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace Mono.Nat.Upnp +{ + internal class ErrorMessage : MessageBase + { + #region Member Variables + public string Description + { + get { return this.description; } + } + private string description; + + public int ErrorCode + { + get { return this.errorCode; } + } + private int errorCode; + #endregion + + + #region Constructors + public ErrorMessage(int errorCode, string description) + :base(null) + { + this.description = description; + this.errorCode = errorCode; + } + #endregion + + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs new file mode 100644 index 000000000..c5d7bce70 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs @@ -0,0 +1,62 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Diagnostics; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetServicesMessage : MessageBase + { + private string servicesDescriptionUrl; + private EndPoint hostAddress; + + public GetServicesMessage(string description, EndPoint hostAddress) + :base(null) + { + if (string.IsNullOrEmpty(description)) + Trace.WriteLine("Description is null"); + + if (hostAddress == null) + Trace.WriteLine("hostaddress is null"); + + this.servicesDescriptionUrl = description; + this.hostAddress = hostAddress; + } + + + public override WebRequest Encode(out byte[] body) + { + HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://" + this.hostAddress.ToString() + this.servicesDescriptionUrl); + req.Headers.Add("ACCEPT-LANGUAGE", "en"); + req.Method = "GET"; + + body = new byte[0]; + return req; + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs b/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs new file mode 100644 index 000000000..da650fb41 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs @@ -0,0 +1,75 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.IO; +using System.Globalization; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class CreatePortMappingMessage : MessageBase + { + #region Private Fields + + private IPAddress localIpAddress; + private Mapping mapping; + + #endregion + + + #region Constructors + public CreatePortMappingMessage(Mapping mapping, IPAddress localIpAddress, UpnpNatDevice device) + : base(device) + { + this.mapping = mapping; + this.localIpAddress = localIpAddress; + } + #endregion + + + public override WebRequest Encode(out byte[] body) + { + CultureInfo culture = CultureInfo.InvariantCulture; + + StringBuilder builder = new StringBuilder(256); + XmlWriter writer = CreateWriter(builder); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", this.mapping.PublicPort.ToString(culture)); + WriteFullElement(writer, "NewProtocol", this.mapping.Protocol == Protocol.Tcp ? "TCP" : "UDP"); + WriteFullElement(writer, "NewInternalPort", this.mapping.PrivatePort.ToString(culture)); + WriteFullElement(writer, "NewInternalClient", this.localIpAddress.ToString()); + WriteFullElement(writer, "NewEnabled", "1"); + WriteFullElement(writer, "NewPortMappingDescription", string.IsNullOrEmpty(mapping.Description) ? "Mono.Nat" : mapping.Description); + WriteFullElement(writer, "NewLeaseDuration", mapping.Lifetime.ToString()); + + writer.Flush(); + return CreateRequest("AddPortMapping", builder.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs b/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs new file mode 100644 index 000000000..d9be89a69 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/DeletePortMappingMessage.cs @@ -0,0 +1,57 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Net; +using System.IO; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class DeletePortMappingMessage : MessageBase + { + private Mapping mapping; + + public DeletePortMappingMessage(Mapping mapping, UpnpNatDevice device) + : base(device) + { + this.mapping = mapping; + } + + public override WebRequest Encode(out byte[] body) + { + StringBuilder builder = new StringBuilder(256); + XmlWriter writer = CreateWriter(builder); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", mapping.PublicPort.ToString(MessageBase.Culture)); + WriteFullElement(writer, "NewProtocol", mapping.Protocol == Protocol.Tcp ? "TCP" : "UDP"); + + writer.Flush(); + return CreateRequest("DeletePortMapping", builder.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs b/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs new file mode 100644 index 000000000..8f97002ea --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetExternalIPAddressMessage.cs @@ -0,0 +1,51 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using System.IO; + +namespace Mono.Nat.Upnp +{ + internal class GetExternalIPAddressMessage : MessageBase + { + + #region Constructors + public GetExternalIPAddressMessage(UpnpNatDevice device) + :base(device) + { + } + #endregion + + + public override WebRequest Encode(out byte[] body) + { + return CreateRequest("GetExternalIPAddress", string.Empty, out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs b/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs new file mode 100644 index 000000000..c0c555881 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetGenericPortMappingEntry.cs @@ -0,0 +1,55 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class GetGenericPortMappingEntry : MessageBase + { + private int index; + + public GetGenericPortMappingEntry(int index, UpnpNatDevice device) + :base(device) + { + this.index = index; + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + StringBuilder sb = new StringBuilder(128); + XmlWriter writer = CreateWriter(sb); + + WriteFullElement(writer, "NewPortMappingIndex", index.ToString()); + + writer.Flush(); + return CreateRequest("GetGenericPortMappingEntry", sb.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs b/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs new file mode 100644 index 000000000..314468ece --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Requests/GetSpecificPortMappingEntryMessage.cs @@ -0,0 +1,60 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetSpecificPortMappingEntryMessage : MessageBase + { + internal Protocol protocol; + internal int externalPort; + + public GetSpecificPortMappingEntryMessage(Protocol protocol, int externalPort, UpnpNatDevice device) + : base(device) + { + this.protocol = protocol; + this.externalPort = externalPort; + } + + public override WebRequest Encode(out byte[] body) + { + StringBuilder sb = new StringBuilder(64); + XmlWriter writer = CreateWriter(sb); + + WriteFullElement(writer, "NewRemoteHost", string.Empty); + WriteFullElement(writer, "NewExternalPort", externalPort.ToString()); + WriteFullElement(writer, "NewProtocol", protocol == Protocol.Tcp ? "TCP" : "UDP"); + writer.Flush(); + + return CreateRequest("GetSpecificPortMappingEntry", sb.ToString(), out body); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs new file mode 100644 index 000000000..e75926b09 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/CreatePortMappingResponseMessage.cs @@ -0,0 +1,46 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +namespace Mono.Nat.Upnp +{ + internal class CreatePortMappingResponseMessage : MessageBase + { + #region Constructors + public CreatePortMappingResponseMessage() + :base(null) + { + } + #endregion + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs new file mode 100644 index 000000000..1fce4eb04 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/DeletePortMappingResponseMessage.cs @@ -0,0 +1,44 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + + + +using System; +namespace Mono.Nat.Upnp +{ + internal class DeletePortMapResponseMessage : MessageBase + { + public DeletePortMapResponseMessage() + :base(null) + { + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotSupportedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs new file mode 100644 index 000000000..ee4b18cd1 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/GetExternalIPAddressResponseMessage.cs @@ -0,0 +1,53 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; + +namespace Mono.Nat.Upnp +{ + internal class GetExternalIPAddressResponseMessage : MessageBase + { + public IPAddress ExternalIPAddress + { + get { return this.externalIPAddress; } + } + private IPAddress externalIPAddress; + + public GetExternalIPAddressResponseMessage(string ip) + :base(null) + { + this.externalIPAddress = IPAddress.Parse(ip); + } + + public override WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs b/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs new file mode 100644 index 000000000..b11bfa027 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/Responses/GetGenericPortMappingEntryResponseMessage.cs @@ -0,0 +1,108 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace Mono.Nat.Upnp +{ + internal class GetGenericPortMappingEntryResponseMessage : MessageBase + { + private string remoteHost; + private int externalPort; + private Protocol protocol; + private int internalPort; + private string internalClient; + private bool enabled; + private string portMappingDescription; + private int leaseDuration; + + public string RemoteHost + { + get { return this.remoteHost; } + } + + public int ExternalPort + { + get { return this.externalPort; } + } + + public Protocol Protocol + { + get { return this.protocol; } + } + + public int InternalPort + { + get { return this.internalPort; } + } + + public string InternalClient + { + get { return this.internalClient; } + } + + public bool Enabled + { + get { return this.enabled; } + } + + public string PortMappingDescription + { + get { return this.portMappingDescription; } + } + + public int LeaseDuration + { + get { return this.leaseDuration; } + } + + + public GetGenericPortMappingEntryResponseMessage(XmlNode data, bool genericMapping) + : base(null) + { + remoteHost = (genericMapping) ? data["NewRemoteHost"].InnerText : string.Empty; + externalPort = (genericMapping) ? Convert.ToInt32(data["NewExternalPort"].InnerText) : -1; + if (genericMapping) + protocol = data["NewProtocol"].InnerText.Equals("TCP", StringComparison.InvariantCultureIgnoreCase) ? Protocol.Tcp : Protocol.Udp; + else + protocol = Protocol.Udp; + + internalPort = Convert.ToInt32(data["NewInternalPort"].InnerText); + internalClient = data["NewInternalClient"].InnerText; + enabled = data["NewEnabled"].InnerText == "1" ? true : false; + portMappingDescription = data["NewPortMappingDescription"].InnerText; + leaseDuration = Convert.ToInt32(data["NewLeaseDuration"].InnerText); + } + + public override System.Net.WebRequest Encode(out byte[] body) + { + throw new NotImplementedException(); + } + } +} diff --git a/Mono.Nat/Upnp/Messages/UpnpMessage.cs b/Mono.Nat/Upnp/Messages/UpnpMessage.cs new file mode 100644 index 000000000..44c16eec6 --- /dev/null +++ b/Mono.Nat/Upnp/Messages/UpnpMessage.cs @@ -0,0 +1,132 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// +// Copyright (C) 2006 Alan McGovern +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Diagnostics; +using System.Xml; +using System.Net; +using System.IO; +using System.Text; +using System.Globalization; + +namespace Mono.Nat.Upnp +{ + internal abstract class MessageBase + { + internal static readonly CultureInfo Culture = CultureInfo.InvariantCulture; + protected UpnpNatDevice device; + + protected MessageBase(UpnpNatDevice device) + { + this.device = device; + } + + protected WebRequest CreateRequest(string upnpMethod, string methodParameters, out byte[] body) + { + string ss = "http://" + this.device.HostEndPoint.ToString() + this.device.ControlUrl; + NatUtility.Log("Initiating request to: {0}", ss); + Uri location = new Uri(ss); + + HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(location); + req.KeepAlive = false; + req.Method = "POST"; + req.ContentType = "text/xml; charset=\"utf-8\""; + req.Headers.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); + + string bodyString = "" + + "" + + "" + + methodParameters + + "" + + "" + + "\r\n\r\n"; + + body = System.Text.Encoding.UTF8.GetBytes(bodyString); + return req; + } + + public static MessageBase Decode(UpnpNatDevice device, string message) + { + XmlNode node; + XmlDocument doc = new XmlDocument(); + doc.LoadXml(message); + + XmlNamespaceManager nsm = new XmlNamespaceManager(doc.NameTable); + + // Error messages should be found under this namespace + nsm.AddNamespace("errorNs", "urn:schemas-upnp-org:control-1-0"); + nsm.AddNamespace("responseNs", device.ServiceType); + + // Check to see if we have a fault code message. + if ((node = doc.SelectSingleNode("//errorNs:UPnPError", nsm)) != null) { + string errorCode = node["errorCode"] != null ? node["errorCode"].InnerText : ""; + string errorDescription = node["errorDescription"] != null ? node["errorDescription"].InnerText : ""; + + return new ErrorMessage(Convert.ToInt32(errorCode, CultureInfo.InvariantCulture), errorDescription); + } + + if ((doc.SelectSingleNode("//responseNs:AddPortMappingResponse", nsm)) != null) + return new CreatePortMappingResponseMessage(); + + if ((doc.SelectSingleNode("//responseNs:DeletePortMappingResponse", nsm)) != null) + return new DeletePortMapResponseMessage(); + + if ((node = doc.SelectSingleNode("//responseNs:GetExternalIPAddressResponse", nsm)) != null) { + string newExternalIPAddress = node["NewExternalIPAddress"] != null ? node["NewExternalIPAddress"].InnerText : ""; + return new GetExternalIPAddressResponseMessage(newExternalIPAddress); + } + + if ((node = doc.SelectSingleNode("//responseNs:GetGenericPortMappingEntryResponse", nsm)) != null) + return new GetGenericPortMappingEntryResponseMessage(node, true); + + if ((node = doc.SelectSingleNode("//responseNs:GetSpecificPortMappingEntryResponse", nsm)) != null) + return new GetGenericPortMappingEntryResponseMessage(node, false); + + NatUtility.Log("Unknown message returned. Please send me back the following XML:"); + NatUtility.Log(message); + return null; + } + + public abstract WebRequest Encode(out byte[] body); + + internal static void WriteFullElement(XmlWriter writer, string element, string value) + { + writer.WriteStartElement(element); + writer.WriteString(value); + writer.WriteEndElement(); + } + + internal static XmlWriter CreateWriter(StringBuilder sb) + { + XmlWriterSettings settings = new XmlWriterSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; + return XmlWriter.Create(sb, settings); + } + } +} diff --git a/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs b/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs new file mode 100644 index 000000000..edc5a5d76 --- /dev/null +++ b/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs @@ -0,0 +1,287 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Text; +using System.Net; +using Mono.Nat.Upnp; +using System.Diagnostics; +using System.Net.Sockets; +using System.Net.NetworkInformation; +using MediaBrowser.Controller.Dlna; + +namespace Mono.Nat +{ + internal class UpnpSearcher : ISearcher + { + private const int SearchPeriod = 5 * 60; // The time in seconds between each search + static UpnpSearcher instance = new UpnpSearcher(); + public static List sockets = CreateSockets(); + + public static UpnpSearcher Instance + { + get { return instance; } + } + + public event EventHandler DeviceFound; + public event EventHandler DeviceLost; + + private List devices; + private Dictionary lastFetched; + private DateTime nextSearch; + private IPEndPoint searchEndpoint; + + UpnpSearcher() + { + devices = new List(); + lastFetched = new Dictionary(); + //searchEndpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900); + searchEndpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900); + } + + static List CreateSockets() + { + List clients = new List(); + try + { + foreach (NetworkInterface n in NetworkInterface.GetAllNetworkInterfaces()) + { + foreach (UnicastIPAddressInformation address in n.GetIPProperties().UnicastAddresses) + { + if (address.Address.AddressFamily == AddressFamily.InterNetwork) + { + try + { + clients.Add(new UdpClient(new IPEndPoint(address.Address, 0))); + } + catch + { + continue; // Move on to the next address. + } + } + } + } + } + catch (Exception) + { + clients.Add(new UdpClient(0)); + } + return clients; + } + + public void Search() + { + foreach (UdpClient s in sockets) + { + try + { + Search(s); + } + catch + { + // Ignore any search errors + } + } + } + + void Search(UdpClient client) + { + nextSearch = DateTime.Now.AddSeconds(SearchPeriod); + byte[] data = DiscoverDeviceMessage.EncodeSSDP(); + + // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2) + for (int i = 0; i < 3; i++) + client.Send(data, data.Length, searchEndpoint); + } + + public IPEndPoint SearchEndpoint + { + get { return searchEndpoint; } + } + + public void Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint) + { + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try + { + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + // We have an internet gateway device now + UpnpNatDevice d = new UpnpNatDevice(localAddress, deviceInfo, endpoint, string.Empty); + + if (devices.Contains(d)) + { + // We already have found this device, so we just refresh it to let people know it's + // Still alive. If a device doesn't respond to a search, we dump it. + devices[devices.IndexOf(d)].LastSeen = DateTime.Now; + } + else + { + + // If we send 3 requests at a time, ensure we only fetch the services list once + // even if three responses are received + if (lastFetched.ContainsKey(endpoint.Address)) + { + DateTime last = lastFetched[endpoint.Address]; + if ((DateTime.Now - last) < TimeSpan.FromSeconds(20)) + return; + } + lastFetched[endpoint.Address] = DateTime.Now; + + // Once we've parsed the information we need, we tell the device to retrieve it's service list + // Once we successfully receive the service list, the callback provided will be invoked. + NatUtility.Log("Fetching service list: {0}", d.HostEndPoint); + d.GetServicesList(DeviceSetupComplete); + } + } + catch (Exception ex) + { + NatUtility.Log("Unhandled exception when trying to decode a device's response Send me the following data: "); + NatUtility.Log("ErrorMessage:"); + NatUtility.Log(ex.Message); + } + } + + public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // Convert it to a string for easy parsing + string dataString = null; + + // No matter what, this method should never throw an exception. If something goes wrong + // we should still be in a position to handle the next reply correctly. + try { + string urn; + dataString = Encoding.UTF8.GetString(response); + + if (NatUtility.Verbose) + NatUtility.Log("UPnP Response: {0}", dataString); + + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + string log = "UPnP Response: Router advertised a '{0}' service"; + StringComparison c = StringComparison.OrdinalIgnoreCase; + if (dataString.IndexOf("urn:schemas-upnp-org:service:WANIPConnection:", c) != -1) { + urn = "urn:schemas-upnp-org:service:WANIPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANIPConnection:1"); + } else if (dataString.IndexOf("urn:schemas-upnp-org:service:WANPPPConnection:", c) != -1) { + urn = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANPPPConnection:"); + } else + return; + + // We have an internet gateway device now + UpnpNatDevice d = new UpnpNatDevice(localAddress, dataString, urn); + + if (devices.Contains(d)) + { + // We already have found this device, so we just refresh it to let people know it's + // Still alive. If a device doesn't respond to a search, we dump it. + devices[devices.IndexOf(d)].LastSeen = DateTime.Now; + } + else + { + + // If we send 3 requests at a time, ensure we only fetch the services list once + // even if three responses are received + if (lastFetched.ContainsKey(endpoint.Address)) + { + DateTime last = lastFetched[endpoint.Address]; + if ((DateTime.Now - last) < TimeSpan.FromSeconds(20)) + return; + } + lastFetched[endpoint.Address] = DateTime.Now; + + // Once we've parsed the information we need, we tell the device to retrieve it's service list + // Once we successfully receive the service list, the callback provided will be invoked. + NatUtility.Log("Fetching service list: {0}", d.HostEndPoint); + d.GetServicesList(DeviceSetupComplete); + } + } + catch (Exception ex) + { + Trace.WriteLine("Unhandled exception when trying to decode a device's response Send me the following data: "); + Trace.WriteLine("ErrorMessage:"); + Trace.WriteLine(ex.Message); + Trace.WriteLine("Data string:"); + Trace.WriteLine(dataString); + } + } + + public DateTime NextSearch + { + get { return nextSearch; } + } + + private void DeviceSetupComplete(INatDevice device) + { + lock (this.devices) + { + // We don't want the same device in there twice + if (devices.Contains(device)) + return; + + devices.Add(device); + } + + OnDeviceFound(new DeviceEventArgs(device)); + } + + private void OnDeviceFound(DeviceEventArgs args) + { + if (DeviceFound != null) + DeviceFound(this, args); + } + + public NatProtocol Protocol + { + get { return NatProtocol.Upnp; } + } + } +} diff --git a/Mono.Nat/Upnp/Upnp.cs b/Mono.Nat/Upnp/Upnp.cs new file mode 100644 index 000000000..e44a51c24 --- /dev/null +++ b/Mono.Nat/Upnp/Upnp.cs @@ -0,0 +1,83 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// Nicholas Terry +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// Copyright (C) 2014 Nicholas Terry +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace Mono.Nat.Upnp +{ + internal class Upnp + { + public UpnpNatDevice Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) + { + // Convert it to a string for easy parsing + string dataString = null; + + + string urn; + dataString = Encoding.UTF8.GetString(response); + + if (NatUtility.Verbose) + NatUtility.Log("UPnP Response: {0}", dataString); + + /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. + Any other device type is no good to us for this purpose. See the IGP overview paper + page 5 for an overview of device types and their hierarchy. + http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ + + /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which + version it is and apply the correct URN. */ + + /* Some routers don't correctly implement the version ID on the URN, so we only search for the type + prefix. */ + + string log = "UPnP Response: Router advertised a '{0}' service"; + StringComparison c = StringComparison.OrdinalIgnoreCase; + if (dataString.IndexOf("urn:schemas-upnp-org:service:WANIPConnection:", c) != -1) + { + urn = "urn:schemas-upnp-org:service:WANIPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANIPConnection:1"); + } + else if (dataString.IndexOf("urn:schemas-upnp-org:service:WANPPPConnection:", c) != -1) + { + urn = "urn:schemas-upnp-org:service:WANPPPConnection:1"; + NatUtility.Log(log, "urn:schemas-upnp-org:service:WANPPPConnection:"); + } + else + throw new NotSupportedException("Received non-supported device type"); + + // We have an internet gateway device now + return new UpnpNatDevice(localAddress, dataString, urn); + } + } +} diff --git a/Mono.Nat/Upnp/UpnpNatDevice.cs b/Mono.Nat/Upnp/UpnpNatDevice.cs new file mode 100644 index 000000000..1160d3ac2 --- /dev/null +++ b/Mono.Nat/Upnp/UpnpNatDevice.cs @@ -0,0 +1,651 @@ +// +// Authors: +// Alan McGovern alan.mcgovern@gmail.com +// Ben Motmans +// +// Copyright (C) 2006 Alan McGovern +// Copyright (C) 2007 Ben Motmans +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Xml; +using System.Text; +using System.Diagnostics; +using MediaBrowser.Controller.Dlna; + +namespace Mono.Nat.Upnp +{ + public sealed class UpnpNatDevice : AbstractNatDevice, IEquatable + { + private EndPoint hostEndPoint; + private IPAddress localAddress; + private string serviceDescriptionUrl; + private string controlUrl; + private string serviceType; + + public override IPAddress LocalAddress + { + get { return localAddress; } + } + + /// + /// The callback to invoke when we are finished setting up the device + /// + private NatDeviceCallback callback; + + internal UpnpNatDevice(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint hostEndPoint, string serviceType) + { + this.LastSeen = DateTime.Now; + this.localAddress = localAddress; + + // Split the string at the "location" section so i can extract the ipaddress and service description url + string locationDetails = deviceInfo.Location.ToString(); + this.serviceType = serviceType; + + // Make sure we have no excess whitespace + locationDetails = locationDetails.Trim(); + + // FIXME: Is this reliable enough. What if we get a hostname as opposed to a proper http address + // Are we going to get addresses with the "http://" attached? + if (locationDetails.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) + { + NatUtility.Log("Found device at: {0}", locationDetails); + // This bit strings out the "http://" from the string + locationDetails = locationDetails.Substring(7); + + this.hostEndPoint = hostEndPoint; + + NatUtility.Log("Parsed device as: {0}", this.hostEndPoint.ToString()); + + // The service description URL is the remainder of the "locationDetails" string. The bit that was originally after the ip + // and port information + this.serviceDescriptionUrl = locationDetails.Substring(locationDetails.IndexOf('/')); + } + else + { + NatUtility.Log("Couldn't decode address. Please send following string to the developer: "); + } + } + + internal UpnpNatDevice (IPAddress localAddress, string deviceDetails, string serviceType) + { + this.LastSeen = DateTime.Now; + this.localAddress = localAddress; + + // Split the string at the "location" section so i can extract the ipaddress and service description url + string locationDetails = deviceDetails.Substring(deviceDetails.IndexOf("Location", StringComparison.InvariantCultureIgnoreCase) + 9).Split('\r')[0]; + this.serviceType = serviceType; + + // Make sure we have no excess whitespace + locationDetails = locationDetails.Trim(); + + // FIXME: Is this reliable enough. What if we get a hostname as opposed to a proper http address + // Are we going to get addresses with the "http://" attached? + if (locationDetails.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) + { + NatUtility.Log("Found device at: {0}", locationDetails); + // This bit strings out the "http://" from the string + locationDetails = locationDetails.Substring(7); + + // We then split off the end of the string to get something like: 192.168.0.3:241 in our string + string hostAddressAndPort = locationDetails.Remove(locationDetails.IndexOf('/')); + + // From this we parse out the IP address and Port + if (hostAddressAndPort.IndexOf(':') > 0) + { + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(hostAddressAndPort.Remove(hostAddressAndPort.IndexOf(':'))), + Convert.ToUInt16(hostAddressAndPort.Substring(hostAddressAndPort.IndexOf(':') + 1), System.Globalization.CultureInfo.InvariantCulture)); + } + else + { + // there is no port specified, use default port (80) + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(hostAddressAndPort), 80); + } + + NatUtility.Log("Parsed device as: {0}", this.hostEndPoint.ToString()); + + // The service description URL is the remainder of the "locationDetails" string. The bit that was originally after the ip + // and port information + this.serviceDescriptionUrl = locationDetails.Substring(locationDetails.IndexOf('/')); + } + else + { + Trace.WriteLine("Couldn't decode address. Please send following string to the developer: "); + Trace.WriteLine(deviceDetails); + } + } + + /// + /// The EndPoint that the device is at + /// + internal EndPoint HostEndPoint + { + get { return this.hostEndPoint; } + } + + /// + /// The relative url of the xml file that describes the list of services is at + /// + internal string ServiceDescriptionUrl + { + get { return this.serviceDescriptionUrl; } + } + + /// + /// The relative url that we can use to control the port forwarding + /// + internal string ControlUrl + { + get { return this.controlUrl; } + } + + /// + /// The service type we're using on the device + /// + public string ServiceType + { + get { return serviceType; } + } + + /// + /// Begins an async call to get the external ip address of the router + /// + public override IAsyncResult BeginGetExternalIP(AsyncCallback callback, object asyncState) + { + // Create the port map message + GetExternalIPAddressMessage message = new GetExternalIPAddressMessage(this); + return BeginMessageInternal(message, callback, asyncState, EndGetExternalIPInternal); + } + + /// + /// Maps the specified port to this computer + /// + public override IAsyncResult BeginCreatePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + CreatePortMappingMessage message = new CreatePortMappingMessage(mapping, localAddress, this); + return BeginMessageInternal(message, callback, asyncState, EndCreatePortMapInternal); + } + + /// + /// Removes a port mapping from this computer + /// + public override IAsyncResult BeginDeletePortMap(Mapping mapping, AsyncCallback callback, object asyncState) + { + DeletePortMappingMessage message = new DeletePortMappingMessage(mapping, this); + return BeginMessageInternal(message, callback, asyncState, EndDeletePortMapInternal); + } + + + public override IAsyncResult BeginGetAllMappings(AsyncCallback callback, object asyncState) + { + GetGenericPortMappingEntry message = new GetGenericPortMappingEntry(0, this); + return BeginMessageInternal(message, callback, asyncState, EndGetAllMappingsInternal); + } + + + public override IAsyncResult BeginGetSpecificMapping (Protocol protocol, int port, AsyncCallback callback, object asyncState) + { + GetSpecificPortMappingEntryMessage message = new GetSpecificPortMappingEntryMessage(protocol, port, this); + return this.BeginMessageInternal(message, callback, asyncState, new AsyncCallback(this.EndGetSpecificMappingInternal)); + } + + /// + /// + /// + /// + public override void EndCreatePortMap(IAsyncResult result) + { + if (result == null) throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + // Check if we need to wait for the operation to finish + if (!result.IsCompleted) + result.AsyncWaitHandle.WaitOne(); + + // If we have a saved exception, it means something went wrong during the mapping + // so we just rethrow the exception and let the user figure out what they should do. + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + //return result.AsyncState as Mapping; + } + + + /// + /// + /// + /// + public override void EndDeletePortMap(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + // Check if we need to wait for the operation to finish + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + // If we have a saved exception, it means something went wrong during the mapping + // so we just rethrow the exception and let the user figure out what they should do. + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + // If all goes well, we just return + //return true; + } + + + public override Mapping[] EndGetAllMappings(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + GetAllMappingsAsyncResult mappingResult = result as GetAllMappingsAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + if (msg.ErrorCode != 713) + throw new MappingException(msg.ErrorCode, msg.Description); + } + + return mappingResult.Mappings.ToArray(); + } + + + /// + /// Ends an async request to get the external ip address of the router + /// + public override IPAddress EndGetExternalIP(IAsyncResult result) + { + if (result == null) throw new ArgumentNullException("result"); + + PortMapAsyncResult mappingResult = result as PortMapAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!result.IsCompleted) + result.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage msg = mappingResult.SavedMessage as ErrorMessage; + throw new MappingException(msg.ErrorCode, msg.Description); + } + + if (mappingResult.SavedMessage == null) + return null; + else + return ((GetExternalIPAddressResponseMessage)mappingResult.SavedMessage).ExternalIPAddress; + } + + + public override Mapping EndGetSpecificMapping(IAsyncResult result) + { + if (result == null) + throw new ArgumentNullException("result"); + + GetAllMappingsAsyncResult mappingResult = result as GetAllMappingsAsyncResult; + if (mappingResult == null) + throw new ArgumentException("Invalid AsyncResult", "result"); + + if (!mappingResult.IsCompleted) + mappingResult.AsyncWaitHandle.WaitOne(); + + if (mappingResult.SavedMessage is ErrorMessage) + { + ErrorMessage message = mappingResult.SavedMessage as ErrorMessage; + if (message.ErrorCode != 0x2ca) + { + throw new MappingException(message.ErrorCode, message.Description); + } + } + if (mappingResult.Mappings.Count == 0) + return new Mapping (Protocol.Tcp, -1, -1); + + return mappingResult.Mappings[0]; + } + + + public override bool Equals(object obj) + { + UpnpNatDevice device = obj as UpnpNatDevice; + return (device == null) ? false : this.Equals((device)); + } + + + public bool Equals(UpnpNatDevice other) + { + return (other == null) ? false : (this.hostEndPoint.Equals(other.hostEndPoint) + //&& this.controlUrl == other.controlUrl + && this.serviceDescriptionUrl == other.serviceDescriptionUrl); + } + + public override int GetHashCode() + { + return (this.hostEndPoint.GetHashCode() ^ this.controlUrl.GetHashCode() ^ this.serviceDescriptionUrl.GetHashCode()); + } + + private IAsyncResult BeginMessageInternal(MessageBase message, AsyncCallback storedCallback, object asyncState, AsyncCallback callback) + { + byte[] body; + WebRequest request = message.Encode(out body); + PortMapAsyncResult mappingResult = PortMapAsyncResult.Create(message, request, storedCallback, asyncState); + + if (body.Length > 0) + { + request.ContentLength = body.Length; + request.BeginGetRequestStream(delegate(IAsyncResult result) { + try + { + Stream s = request.EndGetRequestStream(result); + s.Write(body, 0, body.Length); + request.BeginGetResponse(callback, mappingResult); + } + catch (Exception ex) + { + mappingResult.Complete(ex); + } + }, null); + } + else + { + request.BeginGetResponse(callback, mappingResult); + } + return mappingResult; + } + + private void CompleteMessage(IAsyncResult result) + { + PortMapAsyncResult mappingResult = result.AsyncState as PortMapAsyncResult; + mappingResult.CompletedSynchronously = result.CompletedSynchronously; + mappingResult.Complete(); + } + + private MessageBase DecodeMessageFromResponse(Stream s, long length) + { + StringBuilder data = new StringBuilder(); + int bytesRead = 0; + int totalBytesRead = 0; + byte[] buffer = new byte[10240]; + + // Read out the content of the message, hopefully picking everything up in the case where we have no contentlength + if (length != -1) + { + while (totalBytesRead < length) + { + bytesRead = s.Read(buffer, 0, buffer.Length); + data.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + totalBytesRead += bytesRead; + } + } + else + { + while ((bytesRead = s.Read(buffer, 0, buffer.Length)) != 0) + data.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + } + + // Once we have our content, we need to see what kind of message it is. It'll either a an error + // or a response based on the action we performed. + return MessageBase.Decode(this, data.ToString()); + } + + private void EndCreatePortMapInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndMessageInternal(IAsyncResult result) + { + HttpWebResponse response = null; + PortMapAsyncResult mappingResult = result.AsyncState as PortMapAsyncResult; + + try + { + try + { + response = (HttpWebResponse)mappingResult.Request.EndGetResponse(result); + } + catch (WebException ex) + { + // Even if the request "failed" i want to continue on to read out the response from the router + response = ex.Response as HttpWebResponse; + if (response == null) + mappingResult.SavedMessage = new ErrorMessage((int)ex.Status, ex.Message); + } + if (response != null) + mappingResult.SavedMessage = DecodeMessageFromResponse(response.GetResponseStream(), response.ContentLength); + } + + finally + { + if (response != null) + response.Close(); + } + } + + private void EndDeletePortMapInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndGetAllMappingsInternal(IAsyncResult result) + { + EndMessageInternal(result); + + GetAllMappingsAsyncResult mappingResult = result.AsyncState as GetAllMappingsAsyncResult; + GetGenericPortMappingEntryResponseMessage message = mappingResult.SavedMessage as GetGenericPortMappingEntryResponseMessage; + if (message != null) + { + Mapping mapping = new Mapping (message.Protocol, message.InternalPort, message.ExternalPort, message.LeaseDuration); + mapping.Description = message.PortMappingDescription; + mappingResult.Mappings.Add(mapping); + GetGenericPortMappingEntry next = new GetGenericPortMappingEntry(mappingResult.Mappings.Count, this); + + // It's ok to do this synchronously because we should already be on anther thread + // and this won't block the user. + byte[] body; + WebRequest request = next.Encode(out body); + if (body.Length > 0) + { + request.ContentLength = body.Length; + request.GetRequestStream().Write(body, 0, body.Length); + } + mappingResult.Request = request; + request.BeginGetResponse(EndGetAllMappingsInternal, mappingResult); + return; + } + + CompleteMessage(result); + } + + private void EndGetExternalIPInternal(IAsyncResult result) + { + EndMessageInternal(result); + CompleteMessage(result); + } + + private void EndGetSpecificMappingInternal(IAsyncResult result) + { + EndMessageInternal(result); + + GetAllMappingsAsyncResult mappingResult = result.AsyncState as GetAllMappingsAsyncResult; + GetGenericPortMappingEntryResponseMessage message = mappingResult.SavedMessage as GetGenericPortMappingEntryResponseMessage; + if (message != null) { + Mapping mapping = new Mapping(mappingResult.SpecificMapping.Protocol, message.InternalPort, mappingResult.SpecificMapping.PublicPort, message.LeaseDuration); + mapping.Description = mappingResult.SpecificMapping.Description; + mappingResult.Mappings.Add(mapping); + } + + CompleteMessage(result); + } + + internal void GetServicesList(NatDeviceCallback callback) + { + // Save the callback so i can use it again later when i've finished parsing the services available + this.callback = callback; + + // Create a HTTPWebRequest to download the list of services the device offers + byte[] body; + WebRequest request = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint).Encode(out body); + if (body.Length > 0) + NatUtility.Log("Error: Services Message contained a body"); + request.BeginGetResponse(this.ServicesReceived, request); + } + + private void ServicesReceived(IAsyncResult result) + { + HttpWebResponse response = null; + try + { + int abortCount = 0; + int bytesRead = 0; + byte[] buffer = new byte[10240]; + StringBuilder servicesXml = new StringBuilder(); + XmlDocument xmldoc = new XmlDocument(); + HttpWebRequest request = result.AsyncState as HttpWebRequest; + response = request.EndGetResponse(result) as HttpWebResponse; + Stream s = response.GetResponseStream(); + + if (response.StatusCode != HttpStatusCode.OK) { + NatUtility.Log("{0}: Couldn't get services list: {1}", HostEndPoint, response.StatusCode); + return; // FIXME: This the best thing to do?? + } + + while (true) + { + bytesRead = s.Read(buffer, 0, buffer.Length); + servicesXml.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + try + { + xmldoc.LoadXml(servicesXml.ToString()); + response.Close(); + break; + } + catch (XmlException) + { + // If we can't receive the entire XML within 500ms, then drop the connection + // Unfortunately not all routers supply a valid ContentLength (mine doesn't) + // so this hack is needed to keep testing our recieved data until it gets successfully + // parsed by the xmldoc. Without this, the code will never pick up my router. + if (abortCount++ > 50) + { + response.Close(); + return; + } + NatUtility.Log("{0}: Couldn't parse services list", HostEndPoint); + System.Threading.Thread.Sleep(10); + } + } + + NatUtility.Log("{0}: Parsed services list", HostEndPoint); + XmlNamespaceManager ns = new XmlNamespaceManager(xmldoc.NameTable); + ns.AddNamespace("ns", "urn:schemas-upnp-org:device-1-0"); + XmlNodeList nodes = xmldoc.SelectNodes("//*/ns:serviceList", ns); + + foreach (XmlNode node in nodes) + { + //Go through each service there + foreach (XmlNode service in node.ChildNodes) + { + //If the service is a WANIPConnection, then we have what we want + string type = service["serviceType"].InnerText; + NatUtility.Log("{0}: Found service: {1}", HostEndPoint, type); + StringComparison c = StringComparison.OrdinalIgnoreCase; + // TODO: Add support for version 2 of UPnP. + if (type.Equals("urn:schemas-upnp-org:service:WANPPPConnection:1", c) || + type.Equals("urn:schemas-upnp-org:service:WANIPConnection:1", c)) + { + this.controlUrl = service["controlURL"].InnerText; + NatUtility.Log("{0}: Found upnp service at: {1}", HostEndPoint, controlUrl); + try + { + Uri u = new Uri(controlUrl); + if (u.IsAbsoluteUri) + { + EndPoint old = hostEndPoint; + this.hostEndPoint = new IPEndPoint(IPAddress.Parse(u.Host), u.Port); + NatUtility.Log("{0}: Absolute URI detected. Host address is now: {1}", old, HostEndPoint); + this.controlUrl = controlUrl.Substring(u.GetLeftPart(UriPartial.Authority).Length); + NatUtility.Log("{0}: New control url: {1}", HostEndPoint, controlUrl); + } + } + catch + { + NatUtility.Log("{0}: Assuming control Uri is relative: {1}", HostEndPoint, controlUrl); + } + NatUtility.Log("{0}: Handshake Complete", HostEndPoint); + this.callback(this); + return; + } + } + } + + //If we get here, it means that we didn't get WANIPConnection service, which means no uPnP forwarding + //So we don't invoke the callback, so this device is never added to our lists + } + catch (WebException ex) + { + // Just drop the connection, FIXME: Should i retry? + NatUtility.Log("{0}: Device denied the connection attempt: {1}", HostEndPoint, ex); + } + finally + { + if (response != null) + response.Close(); + } + } + + /// + /// Overridden. + /// + /// + public override string ToString( ) + { + //GetExternalIP is blocking and can throw exceptions, can't use it here. + return String.Format( + "UpnpNatDevice - EndPoint: {0}, External IP: {1}, Control Url: {2}, Service Description Url: {3}, Service Type: {4}, Last Seen: {5}", + this.hostEndPoint, "Manually Check" /*this.GetExternalIP()*/, this.controlUrl, this.serviceDescriptionUrl, this.serviceType, this.LastSeen); + } + } +} \ No newline at end of file