diff options
Diffstat (limited to 'Tools/Hazel-Networking/Hazel/UPnP')
-rw-r--r-- | Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs | 65 | ||||
-rw-r--r-- | Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs | 158 | ||||
-rw-r--r-- | Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs | 347 |
3 files changed, 570 insertions, 0 deletions
diff --git a/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs b/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs new file mode 100644 index 0000000..3c7abcf --- /dev/null +++ b/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs @@ -0,0 +1,65 @@ +using System; + +namespace Hazel +{ + public interface ILogger + { + void WriteVerbose(string msg); + void WriteError(string msg); + void WriteWarning(string msg); + void WriteInfo(string msg); + } + + public class NullLogger : ILogger + { + public static readonly NullLogger Instance = new NullLogger(); + + public void WriteVerbose(string msg) + { + } + + public void WriteError(string msg) + { + } + + public void WriteWarning(string msg) + { + } + + public void WriteInfo(string msg) + { + } + } + + public class ConsoleLogger : ILogger + { + private bool verbose; + public ConsoleLogger(bool verbose) + { + this.verbose = verbose; + } + + public void WriteVerbose(string msg) + { + if (this.verbose) + { + Console.WriteLine($"{DateTime.Now} [VERBOSE] {msg}"); + } + } + + public void WriteWarning(string msg) + { + Console.WriteLine($"{DateTime.Now} [WARN] {msg}"); + } + + public void WriteError(string msg) + { + Console.WriteLine($"{DateTime.Now} [ERROR] {msg}"); + } + + public void WriteInfo(string msg) + { + Console.WriteLine($"{DateTime.Now} [INFO] {msg}"); + } + } +} diff --git a/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs b/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs new file mode 100644 index 0000000..d856823 --- /dev/null +++ b/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Hazel.UPnP +{ + internal class NetUtility + { + private static IList<NetworkInterface> GetValidNetworkInterfaces() + { + var nics = NetworkInterface.GetAllNetworkInterfaces(); + if (nics == null || nics.Length < 1) + return new NetworkInterface[0]; + + var validInterfaces = new List<NetworkInterface>(nics.Length); + + NetworkInterface best = null; + foreach (NetworkInterface adapter in nics) + { + if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback || adapter.NetworkInterfaceType == NetworkInterfaceType.Unknown) + continue; + if (!adapter.Supports(NetworkInterfaceComponent.IPv4) && !adapter.Supports(NetworkInterfaceComponent.IPv6)) + continue; + if (best == null) + best = adapter; + if (adapter.OperationalStatus != OperationalStatus.Up) + continue; + + // make sure this adapter has any ip addresses + IPInterfaceProperties properties = adapter.GetIPProperties(); + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null) + { + // Yes it does, add this network interface. + validInterfaces.Add(adapter); + break; + } + } + } + + if (validInterfaces.Count == 0 && best != null) + validInterfaces.Add(best); + + return validInterfaces; + } + + /// <summary> + /// Gets the addresses from all active network interfaces, but at most one per interface. + /// </summary> + /// <param name="addressFamily">The <see cref="AddressFamily"/> of the addresses to return</param> + /// <returns>An <see cref="ICollection{T}"/> of <see cref="UnicastIPAddressInformation"/>.</returns> + public static ICollection<UnicastIPAddressInformation> GetAddressesFromNetworkInterfaces(AddressFamily addressFamily) + { + var unicastAddresses = new List<UnicastIPAddressInformation>(); + + foreach (NetworkInterface adapter in GetValidNetworkInterfaces()) + { + IPInterfaceProperties properties = adapter.GetIPProperties(); + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == addressFamily) + { + unicastAddresses.Add(unicastAddress); + break; + } + } + } + + return unicastAddresses; + } + + /// <summary> + /// Gets my local IPv4 address (not necessarily external) and subnet mask + /// </summary> + public static IPAddress GetMyAddress(out IPAddress mask) + { + var networkInterfaces = GetValidNetworkInterfaces(); + IPInterfaceProperties properties = null; + + if (networkInterfaces.Count > 0) + properties = networkInterfaces[0]?.GetIPProperties(); + + if (properties != null) + { + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + mask = unicastAddress.IPv4Mask; + return unicastAddress.Address; + } + } + } + + mask = null; + return null; + } + + /// <summary> + /// Gets the broadcast address for the first network interface or, if not able to, + /// the limited broadcast address. + /// </summary> + /// <returns>An <see cref="IPAddress"/> for broadcasting.</returns> + public static IPAddress GetBroadcastAddress() + { + var networkInterfaces = GetValidNetworkInterfaces(); + IPInterfaceProperties properties = null; + + if (networkInterfaces.Count > 0) + properties = networkInterfaces[0]?.GetIPProperties(); + + if (properties != null) + { + foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses) + { + IPAddress ipAddress = GetBroadcastAddress(unicastAddress); + if (ipAddress != null) + { + return ipAddress; + } + } + } + + return IPAddress.Broadcast; + } + + /// <summary> + /// Gets the broadcast address for the given <paramref name="unicastAddress"/>. + /// </summary> + /// <param name="unicastAddress">A <see cref="UnicastIPAddressInformation"/></param> + /// <returns>An <see cref="IPAddress"/> for broadcasting, null if the <paramref name="unicastAddress"/> + /// is not an IPv4 address.</returns> + public static IPAddress GetBroadcastAddress(UnicastIPAddressInformation unicastAddress) + { + if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork) + { + var mask = unicastAddress.IPv4Mask; + byte[] ipAdressBytes = unicastAddress.Address.GetAddressBytes(); + byte[] subnetMaskBytes = mask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) + { + broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); + } + return new IPAddress(broadcastAddress); + } + + return null; + } + } +} diff --git a/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs b/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs new file mode 100644 index 0000000..771709e --- /dev/null +++ b/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs @@ -0,0 +1,347 @@ +using System; +using System.IO; +using System.Xml; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Hazel.UPnP +{ + /// <summary> + /// Status of the UPnP capabilities + /// </summary> + public enum UPnPStatus + { + /// <summary> + /// Still discovering UPnP capabilities + /// </summary> + Discovering, + + /// <summary> + /// UPnP is not available + /// </summary> + NotAvailable, + + /// <summary> + /// UPnP is available and ready to use + /// </summary> + Available + } + + public class UPnPHelper : IDisposable + { + private const int DiscoveryTimeOutMs = 1000; + + private string serviceUrl; + private string serviceName = ""; + + private ManualResetEvent discoveryComplete = new ManualResetEvent(false); + private Socket socket; + + private DateTime discoveryResponseDeadline; + + private EndPoint ep; + private byte[] buffer; + + private ILogger logger; + + /// <summary> + /// Status of the UPnP capabilities of this NetPeer + /// </summary> + public UPnPStatus Status { get; private set; } + + public UPnPHelper(ILogger logger) + { + this.logger = logger; + + this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + this.socket.EnableBroadcast = true; + this.socket.MulticastLoopback = false; + + this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); + this.socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + + this.ep = new IPEndPoint(IPAddress.Any, 1900); + this.buffer = new byte[ushort.MaxValue]; + + ListenForUPnP(); + + this.Discover(); + } + + private void ListenForUPnP() + { + try + { + socket.BeginReceiveFrom(this.buffer, 0, this.buffer.Length, SocketFlags.None, ref ep, HandleMessage, null); + } + catch(Exception e) + { + this.logger.WriteInfo("Exception listening for UPnP: " + e.Message); + } + } + + private void HandleMessage(IAsyncResult ar) + { + int len; + try + { + len = this.socket.EndReceiveFrom(ar, ref ep); + } + catch + { + return; + } + + string resp = System.Text.Encoding.UTF8.GetString(buffer, 0, len); + if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) + { + var locationStart = resp.IndexOf("location:", StringComparison.OrdinalIgnoreCase); + if (locationStart >= 0) + { + locationStart += 10; + var locationEnd = resp.IndexOf("\r", locationStart); + + resp = resp.Substring(locationStart, locationEnd - locationStart); + if (!ExtractServiceUrl(resp)) + { + ListenForUPnP(); + } + } + else + { + ListenForUPnP(); + } + } + else + { + ListenForUPnP(); + } + } + + internal void Discover() + { + string str = +"M-SEARCH * HTTP/1.1\r\n" + +"HOST: 239.255.255.250:1900\r\n" + +"ST:upnp:rootdevice\r\n" + +"MAN:\"ssdp:discover\"\r\n" + +"MX:3\r\n\r\n"; + + discoveryResponseDeadline = DateTime.UtcNow.AddSeconds(6); + Status = UPnPStatus.Discovering; + + byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str); + + this.logger.WriteInfo("Attempting UPnP discovery"); + + socket.SendTo(buffer, new IPEndPoint(NetUtility.GetBroadcastAddress(), 1900)); + } + + internal bool ExtractServiceUrl(string resp) + { + try + { + XmlDocument desc = new XmlDocument(); + using (var response = WebRequest.Create(resp).GetResponse()) + { + desc.Load(response.GetResponseStream()); + } + + XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr); + if (!typen.Value.Contains("InternetGatewayDevice")) + return false; + + serviceName = "WANIPConnection"; + XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + serviceName + ":1\"]/tns:controlURL/text()", nsMgr); + if (node == null) + { + //try another service name + serviceName = "WANPPPConnection"; + node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + serviceName + ":1\"]/tns:controlURL/text()", nsMgr); + if (node == null) + return false; + } + + serviceUrl = CombineUrls(resp, node.Value); + this.logger.WriteInfo("UPnP service ready"); + Status = UPnPStatus.Available; + discoveryComplete.Set(); + return true; + } + catch (Exception e) + { + this.logger.WriteError("Exception while parsing UPnP Service URL: " + e.Message); + return false; + } + } + + private static string CombineUrls(string gatewayURL, string subURL) + { + // Is Control URL an absolute URL? + if (subURL.Contains("http:") || subURL.Contains(".")) + return subURL; + + gatewayURL = gatewayURL.Replace("http://", ""); // strip any protocol + int n = gatewayURL.IndexOf("/"); + if (n >= 0) + { + gatewayURL = gatewayURL.Substring(0, n); // Use first portion of URL + } + + return "http://" + gatewayURL + subURL; + } + + private bool CheckAvailability() + { + switch (Status) + { + case UPnPStatus.NotAvailable: + return false; + case UPnPStatus.Available: + return true; + case UPnPStatus.Discovering: + while (!discoveryComplete.WaitOne(DiscoveryTimeOutMs)) + { + if (DateTime.UtcNow > discoveryResponseDeadline) + { + Status = UPnPStatus.NotAvailable; + return false; + } + } + + return true; + } + + return false; + } + + /// <summary> + /// Add a forwarding rule to the router using UPnP + /// </summary> + /// <param name="externalPort">The external, WAN facing, port</param> + /// <param name="description">A description for the port forwarding rule</param> + /// <param name="internalPort">The port on the client machine to send traffic to</param> + /// <param name="durationSeconds">The lease duration on the port forwarding rule, in seconds. 0 for indefinite.</param> + public bool ForwardPort(int externalPort, string description, int internalPort = 0, int durationSeconds = 0) + { + if (!CheckAvailability()) + return false; + + if (internalPort == 0) + internalPort = externalPort; + + try + { + var client = NetUtility.GetMyAddress(out _); + if (client == null) + return false; + + SOAPRequest(serviceUrl, + $"<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:{serviceName}:1\">" + + "<NewRemoteHost></NewRemoteHost>" + + $"<NewExternalPort>{externalPort}</NewExternalPort>" + + "<NewProtocol>UDP</NewProtocol>" + + $"<NewInternalPort>{internalPort}</NewInternalPort>" + + $"<NewInternalClient>{client}</NewInternalClient>" + + "<NewEnabled>1</NewEnabled>" + + $"<NewPortMappingDescription>{description}</NewPortMappingDescription>" + + $"<NewLeaseDuration>{durationSeconds}</NewLeaseDuration>" + + "</u:AddPortMapping>", + "AddPortMapping"); + + this.logger.WriteInfo("Sent UPnP port forward request."); + return true; + } + catch (Exception ex) + { + this.logger.WriteError("UPnP port forward failed: " + ex.Message); + return false; + } + } + + /// <summary> + /// Delete a forwarding rule from the router using UPnP + /// </summary> + /// <param name="externalPort">The external, 'internet facing', port</param> + public bool DeleteForwardingRule(int externalPort) + { + if (!CheckAvailability()) + return false; + + try + { + SOAPRequest(serviceUrl, + $"<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:{serviceName}:1\">" + + "<NewRemoteHost></NewRemoteHost>" + + $"<NewExternalPort>{externalPort}</NewExternalPort>" + + $"<NewProtocol>UDP</NewProtocol>" + + "</u:DeletePortMapping>", "DeletePortMapping"); + return true; + } + catch (Exception ex) + { + // m_peer.LogWarning("UPnP delete forwarding rule failed: " + ex.Message); + return false; + } + } + + /// <summary> + /// Retrieve the extern ip using UPnP + /// </summary> + public IPAddress GetExternalIP() + { + if (!CheckAvailability()) + return null; + try + { + XmlDocument xdoc = SOAPRequest(serviceUrl, "<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:" + serviceName + ":1\">" + + "</u:GetExternalIPAddress>", "GetExternalIPAddress"); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value; + return IPAddress.Parse(IP); + } + catch (Exception ex) + { + // m_peer.LogWarning("Failed to get external IP: " + ex.Message); + return null; + } + } + + private XmlDocument SOAPRequest(string url, string soap, string function) + { + string req = +"<?xml version=\"1.0\"?>" + +"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + +$"<s:Body>{soap}</s:Body>" + +"</s:Envelope>"; + + WebRequest r = HttpWebRequest.Create(url); + r.Headers.Add("SOAPACTION", $"\"urn:schemas-upnp-org:service:{serviceName}:1#{function}\""); + r.ContentType = "text/xml; charset=\"utf-8\""; + r.Method = "POST"; + + byte[] b = System.Text.Encoding.UTF8.GetBytes(req); + r.ContentLength = b.Length; + r.GetRequestStream().Write(b, 0, b.Length); + + using (WebResponse wres = r.GetResponse()) + { + XmlDocument resp = new XmlDocument(); + Stream ress = wres.GetResponseStream(); + resp.Load(ress); + return resp; + } + } + + public void Dispose() + { + this.discoveryComplete.Dispose(); + try { this.socket.Shutdown(SocketShutdown.Both); } catch { } + this.socket.Dispose(); + } + } +}
\ No newline at end of file |