From 8d2a2cd5de40e2b94ef5007c32832ed9a063dc40 Mon Sep 17 00:00:00 2001
From: chai <215380520@qq.com>
Date: Thu, 12 Oct 2023 22:09:49 +0800
Subject: +hazel-networking
---
Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs | 347 ++++++++++++++++++++++++
1 file changed, 347 insertions(+)
create mode 100644 Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs
(limited to 'Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs')
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
+{
+ ///
+ /// 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();
+ }
+ }
+}
\ No newline at end of file
--
cgit v1.1-26-g67d0