github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/p2p/upnp/upnp.go (about)

     1  // Taken from taipei-torrent.
     2  // Just enough UPnP to be able to forward ports
     3  // For more information, see: http://www.upnp-hacks.org/upnp.html
     4  package upnp
     5  
     6  // TODO: use syscalls to get actual ourIP, see issue #712
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/xml"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"net"
    15  	"net/http"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  )
    20  
    21  type upnpNAT struct {
    22  	serviceURL string
    23  	ourIP      string
    24  	urnDomain  string
    25  }
    26  
    27  // protocol is either "udp" or "tcp"
    28  type NAT interface {
    29  	GetExternalAddress() (addr net.IP, err error)
    30  	AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error)
    31  	DeletePortMapping(protocol string, externalPort, internalPort int) (err error)
    32  }
    33  
    34  func Discover() (nat NAT, err error) {
    35  	ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900")
    36  	if err != nil {
    37  		return
    38  	}
    39  	conn, err := net.ListenPacket("udp4", ":0")
    40  	if err != nil {
    41  		return
    42  	}
    43  	socket := conn.(*net.UDPConn)
    44  	defer socket.Close() //nolint: errcheck
    45  
    46  	if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	st := "InternetGatewayDevice:1"
    51  
    52  	buf := bytes.NewBufferString(
    53  		"M-SEARCH * HTTP/1.1\r\n" +
    54  			"HOST: 239.255.255.250:1900\r\n" +
    55  			"ST: ssdp:all\r\n" +
    56  			"MAN: \"ssdp:discover\"\r\n" +
    57  			"MX: 2\r\n\r\n")
    58  	message := buf.Bytes()
    59  	answerBytes := make([]byte, 1024)
    60  	for i := 0; i < 3; i++ {
    61  		_, err = socket.WriteToUDP(message, ssdp)
    62  		if err != nil {
    63  			return
    64  		}
    65  		var n int
    66  		_, _, err = socket.ReadFromUDP(answerBytes)
    67  		if err != nil {
    68  			return
    69  		}
    70  		for {
    71  			n, _, err = socket.ReadFromUDP(answerBytes)
    72  			if err != nil {
    73  				break
    74  			}
    75  			answer := string(answerBytes[0:n])
    76  			if !strings.Contains(answer, st) {
    77  				continue
    78  			}
    79  			// HTTP header field names are case-insensitive.
    80  			// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
    81  			locString := "\r\nlocation:"
    82  			answer = strings.ToLower(answer)
    83  			locIndex := strings.Index(answer, locString)
    84  			if locIndex < 0 {
    85  				continue
    86  			}
    87  			loc := answer[locIndex+len(locString):]
    88  			endIndex := strings.Index(loc, "\r\n")
    89  			if endIndex < 0 {
    90  				continue
    91  			}
    92  			locURL := strings.TrimSpace(loc[0:endIndex])
    93  			var serviceURL, urnDomain string
    94  			serviceURL, urnDomain, err = getServiceURL(locURL)
    95  			if err != nil {
    96  				return
    97  			}
    98  			var ourIP net.IP
    99  			ourIP, err = localIPv4()
   100  			if err != nil {
   101  				return
   102  			}
   103  			nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain}
   104  			return
   105  		}
   106  	}
   107  	err = errors.New("UPnP port discovery failed")
   108  	return nat, err
   109  }
   110  
   111  type Envelope struct {
   112  	XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
   113  	Soap    *SoapBody
   114  }
   115  
   116  type SoapBody struct {
   117  	XMLName    xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
   118  	ExternalIP *ExternalIPAddressResponse
   119  }
   120  
   121  type ExternalIPAddressResponse struct {
   122  	XMLName   xml.Name `xml:"GetExternalIPAddressResponse"`
   123  	IPAddress string   `xml:"NewExternalIPAddress"`
   124  }
   125  
   126  type ExternalIPAddress struct {
   127  	XMLName xml.Name `xml:"NewExternalIPAddress"`
   128  	IP      string
   129  }
   130  
   131  type UPNPService struct {
   132  	ServiceType string `xml:"serviceType"`
   133  	ControlURL  string `xml:"controlURL"`
   134  }
   135  
   136  type DeviceList struct {
   137  	Device []Device `xml:"device"`
   138  }
   139  
   140  type ServiceList struct {
   141  	Service []UPNPService `xml:"service"`
   142  }
   143  
   144  type Device struct {
   145  	XMLName     xml.Name    `xml:"device"`
   146  	DeviceType  string      `xml:"deviceType"`
   147  	DeviceList  DeviceList  `xml:"deviceList"`
   148  	ServiceList ServiceList `xml:"serviceList"`
   149  }
   150  
   151  type Root struct {
   152  	Device Device
   153  }
   154  
   155  func getChildDevice(d *Device, deviceType string) *Device {
   156  	dl := d.DeviceList.Device
   157  	for i := 0; i < len(dl); i++ {
   158  		if strings.Contains(dl[i].DeviceType, deviceType) {
   159  			return &dl[i]
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  func getChildService(d *Device, serviceType string) *UPNPService {
   166  	sl := d.ServiceList.Service
   167  	for i := 0; i < len(sl); i++ {
   168  		if strings.Contains(sl[i].ServiceType, serviceType) {
   169  			return &sl[i]
   170  		}
   171  	}
   172  	return nil
   173  }
   174  
   175  func localIPv4() (net.IP, error) {
   176  	tt, err := net.Interfaces()
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	for _, t := range tt {
   181  		aa, err := t.Addrs()
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		for _, a := range aa {
   186  			ipnet, ok := a.(*net.IPNet)
   187  			if !ok {
   188  				continue
   189  			}
   190  			v4 := ipnet.IP.To4()
   191  			if v4 == nil || v4[0] == 127 { // loopback address
   192  				continue
   193  			}
   194  			return v4, nil
   195  		}
   196  	}
   197  	return nil, errors.New("cannot find local IP address")
   198  }
   199  
   200  func getServiceURL(rootURL string) (url, urnDomain string, err error) {
   201  	r, err := http.Get(rootURL) //nolint: gosec
   202  	if err != nil {
   203  		return
   204  	}
   205  	defer r.Body.Close() //nolint: errcheck
   206  
   207  	if r.StatusCode >= 400 {
   208  		err = errors.New(fmt.Sprint(r.StatusCode))
   209  		return
   210  	}
   211  	var root Root
   212  	err = xml.NewDecoder(r.Body).Decode(&root)
   213  	if err != nil {
   214  		return
   215  	}
   216  	a := &root.Device
   217  	if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") {
   218  		err = errors.New("no InternetGatewayDevice")
   219  		return
   220  	}
   221  	b := getChildDevice(a, "WANDevice:1")
   222  	if b == nil {
   223  		err = errors.New("no WANDevice")
   224  		return
   225  	}
   226  	c := getChildDevice(b, "WANConnectionDevice:1")
   227  	if c == nil {
   228  		err = errors.New("no WANConnectionDevice")
   229  		return
   230  	}
   231  	d := getChildService(c, "WANIPConnection:1")
   232  	if d == nil {
   233  		// Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice,
   234  		// instead of under WanConnectionDevice
   235  		d = getChildService(b, "WANIPConnection:1")
   236  
   237  		if d == nil {
   238  			err = errors.New("no WANIPConnection")
   239  			return
   240  		}
   241  	}
   242  	// Extract the domain name, which isn't always 'schemas-upnp-org'
   243  	urnDomain = strings.Split(d.ServiceType, ":")[1]
   244  	url = combineURL(rootURL, d.ControlURL)
   245  	return url, urnDomain, err
   246  }
   247  
   248  func combineURL(rootURL, subURL string) string {
   249  	protocolEnd := "://"
   250  	protoEndIndex := strings.Index(rootURL, protocolEnd)
   251  	a := rootURL[protoEndIndex+len(protocolEnd):]
   252  	rootIndex := strings.Index(a, "/")
   253  	return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL
   254  }
   255  
   256  func soapRequest(url, function, message, domain string) (r *http.Response, err error) {
   257  	fullMessage := "<?xml version=\"1.0\" ?>" +
   258  		"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" +
   259  		"<s:Body>" + message + "</s:Body></s:Envelope>"
   260  
   261  	req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage))
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"")
   266  	req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3")
   267  	// req.Header.Set("Transfer-Encoding", "chunked")
   268  	req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"")
   269  	req.Header.Set("Connection", "Close")
   270  	req.Header.Set("Cache-Control", "no-cache")
   271  	req.Header.Set("Pragma", "no-cache")
   272  
   273  	// log.Stderr("soapRequest ", req)
   274  
   275  	r, err = http.DefaultClient.Do(req)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	/*if r.Body != nil {
   280  	    defer r.Body.Close()
   281  	}*/
   282  
   283  	if r.StatusCode >= 400 {
   284  		// log.Stderr(function, r.StatusCode)
   285  		err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function)
   286  		r = nil
   287  		return
   288  	}
   289  	return r, err
   290  }
   291  
   292  type statusInfo struct {
   293  	externalIPAddress string
   294  }
   295  
   296  func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) {
   297  	message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
   298  		"</u:GetExternalIPAddress>"
   299  
   300  	var response *http.Response
   301  	response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain)
   302  	if response != nil {
   303  		defer response.Body.Close() //nolint: errcheck
   304  	}
   305  	if err != nil {
   306  		return
   307  	}
   308  	var envelope Envelope
   309  	data, err := io.ReadAll(response.Body)
   310  	if err != nil {
   311  		return
   312  	}
   313  	reader := bytes.NewReader(data)
   314  	err = xml.NewDecoder(reader).Decode(&envelope)
   315  	if err != nil {
   316  		return
   317  	}
   318  
   319  	info = statusInfo{envelope.Soap.ExternalIP.IPAddress}
   320  
   321  	if err != nil {
   322  		return
   323  	}
   324  
   325  	return info, err
   326  }
   327  
   328  // GetExternalAddress returns an external IP. If GetExternalIPAddress action
   329  // fails or IP returned is invalid, GetExternalAddress returns an error.
   330  func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
   331  	info, err := n.getExternalIPAddress()
   332  	if err != nil {
   333  		return
   334  	}
   335  	addr = net.ParseIP(info.externalIPAddress)
   336  	if addr == nil {
   337  		err = fmt.Errorf("failed to parse IP: %v", info.externalIPAddress)
   338  	}
   339  	return
   340  }
   341  
   342  func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) {
   343  	// A single concatenation would break ARM compilation.
   344  	message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
   345  		"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
   346  	message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>"
   347  	message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
   348  		"<NewInternalClient>" + n.ourIP + "</NewInternalClient>" +
   349  		"<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
   350  	message += description +
   351  		"</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
   352  		"</NewLeaseDuration></u:AddPortMapping>"
   353  
   354  	var response *http.Response
   355  	response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain)
   356  	if response != nil {
   357  		defer response.Body.Close() //nolint: errcheck
   358  	}
   359  	if err != nil {
   360  		return
   361  	}
   362  
   363  	// TODO: check response to see if the port was forwarded
   364  	// log.Println(message, response)
   365  	// JAE:
   366  	// body, err := io.ReadAll(response.Body)
   367  	// fmt.Println(string(body), err)
   368  	mappedExternalPort = externalPort
   369  	_ = response
   370  	return
   371  }
   372  
   373  func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {
   374  	message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
   375  		"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
   376  		"</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" +
   377  		"</u:DeletePortMapping>"
   378  
   379  	var response *http.Response
   380  	response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
   381  	if response != nil {
   382  		defer response.Body.Close() //nolint: errcheck
   383  	}
   384  	if err != nil {
   385  		return
   386  	}
   387  
   388  	// TODO: check response to see if the port was deleted
   389  	// log.Println(message, response)
   390  	_ = response
   391  	return
   392  }