using System; using System.IO; using System.Xml; using System.Net; using System.Net.Sockets; using System.Threading; namespace Hazel.UPnP { /// /// Status of the UPnP capabilities /// public enum UPnPStatus { /// /// Still discovering UPnP capabilities /// Discovering, /// /// UPnP is not available /// NotAvailable, /// /// UPnP is available and ready to use /// 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; /// /// Status of the UPnP capabilities of this NetPeer /// 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; } /// /// Add a forwarding rule to the router using UPnP /// /// The external, WAN facing, port /// A description for the port forwarding rule /// The port on the client machine to send traffic to /// The lease duration on the port forwarding rule, in seconds. 0 for indefinite. 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, $"" + "" + $"{externalPort}" + "UDP" + $"{internalPort}" + $"{client}" + "1" + $"{description}" + $"{durationSeconds}" + "", "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; } } /// /// Delete a forwarding rule from the router using UPnP /// /// The external, 'internet facing', port public bool DeleteForwardingRule(int externalPort) { if (!CheckAvailability()) return false; try { SOAPRequest(serviceUrl, $"" + "" + $"{externalPort}" + $"UDP" + "", "DeletePortMapping"); return true; } catch (Exception ex) { // m_peer.LogWarning("UPnP delete forwarding rule failed: " + ex.Message); return false; } } /// /// Retrieve the extern ip using UPnP /// public IPAddress GetExternalIP() { if (!CheckAvailability()) return null; try { XmlDocument xdoc = SOAPRequest(serviceUrl, "" + "", "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 = "" + "" + $"{soap}" + ""; 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(); } } }