github.com/kayoticsully/syncthing@v0.8.9-0.20140724133906-c45a2fdc03f8/upnp/upnp.go (about)

     1  // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
     2  // All rights reserved. Use of this source code is governed by an MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go
     6  // Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE)
     7  
     8  // Package upnp implements UPnP Internet Gateway upnpDevice port mappings
     9  package upnp
    10  
    11  import (
    12  	"bufio"
    13  	"bytes"
    14  	"encoding/xml"
    15  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"io/ioutil"
    19  	"net"
    20  	"net/http"
    21  	"net/url"
    22  	"strings"
    23  	"time"
    24  )
    25  
    26  type IGD struct {
    27  	serviceURL string
    28  	device     string
    29  	ourIP      string
    30  }
    31  
    32  type Protocol string
    33  
    34  const (
    35  	TCP Protocol = "TCP"
    36  	UDP          = "UDP"
    37  )
    38  
    39  type upnpService struct {
    40  	ServiceType string `xml:"serviceType"`
    41  	ControlURL  string `xml:"controlURL"`
    42  }
    43  
    44  type upnpDevice struct {
    45  	DeviceType string        `xml:"deviceType"`
    46  	Devices    []upnpDevice  `xml:"deviceList>device"`
    47  	Services   []upnpService `xml:"serviceList>service"`
    48  }
    49  
    50  type upnpRoot struct {
    51  	Device upnpDevice `xml:"device"`
    52  }
    53  
    54  func Discover() (*IGD, error) {
    55  	ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
    56  
    57  	socket, err := net.ListenUDP("udp4", &net.UDPAddr{})
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	defer socket.Close()
    62  
    63  	err = socket.SetDeadline(time.Now().Add(3 * time.Second))
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	searchStr := `M-SEARCH * HTTP/1.1
    69  Host: 239.255.255.250:1900
    70  St: urn:schemas-upnp-org:device:InternetGatewayDevice:1
    71  Man: "ssdp:discover"
    72  Mx: 3
    73  
    74  `
    75  	search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
    76  
    77  	_, err = socket.WriteTo(search, ssdp)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	resp := make([]byte, 1500)
    83  	n, _, err := socket.ReadFrom(resp)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	if debug {
    89  		l.Debugln(string(resp[:n]))
    90  	}
    91  
    92  	reader := bufio.NewReader(bytes.NewBuffer(resp[:n]))
    93  	request := &http.Request{}
    94  	response, err := http.ReadResponse(reader, request)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	if response.Header.Get("St") != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
   100  		return nil, errors.New("no igd")
   101  	}
   102  
   103  	locURL := response.Header.Get("Location")
   104  	if locURL == "" {
   105  		return nil, errors.New("no location")
   106  	}
   107  
   108  	serviceURL, device, err := getServiceURL(locURL)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	// Figure out our IP number, on the network used to reach the IGD. We
   114  	// do this in a fairly roundabout way by connecting to the IGD and
   115  	// checking the address of the local end of the socket. I'm open to
   116  	// suggestions on a better way to do this...
   117  	ourIP, err := localIP(locURL)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	igd := &IGD{
   123  		serviceURL: serviceURL,
   124  		device:     device,
   125  		ourIP:      ourIP,
   126  	}
   127  	return igd, nil
   128  }
   129  
   130  func localIP(tgt string) (string, error) {
   131  	url, err := url.Parse(tgt)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  
   136  	conn, err := net.Dial("tcp", url.Host)
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  	defer conn.Close()
   141  
   142  	ourIP, _, err := net.SplitHostPort(conn.LocalAddr().String())
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  
   147  	return ourIP, nil
   148  }
   149  
   150  func getChildDevice(d upnpDevice, deviceType string) (upnpDevice, bool) {
   151  	for _, dev := range d.Devices {
   152  		if dev.DeviceType == deviceType {
   153  			return dev, true
   154  		}
   155  	}
   156  	return upnpDevice{}, false
   157  }
   158  
   159  func getChildService(d upnpDevice, serviceType string) (upnpService, bool) {
   160  	for _, svc := range d.Services {
   161  		if svc.ServiceType == serviceType {
   162  			return svc, true
   163  		}
   164  	}
   165  	return upnpService{}, false
   166  }
   167  
   168  func getServiceURL(rootURL string) (string, string, error) {
   169  	r, err := http.Get(rootURL)
   170  	if err != nil {
   171  		return "", "", err
   172  	}
   173  	defer r.Body.Close()
   174  	if r.StatusCode >= 400 {
   175  		return "", "", errors.New(r.Status)
   176  	}
   177  	return getServiceURLReader(rootURL, r.Body)
   178  }
   179  
   180  func getServiceURLReader(rootURL string, r io.Reader) (string, string, error) {
   181  	var upnpRoot upnpRoot
   182  	err := xml.NewDecoder(r).Decode(&upnpRoot)
   183  	if err != nil {
   184  		return "", "", err
   185  	}
   186  
   187  	dev := upnpRoot.Device
   188  	if dev.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
   189  		return "", "", errors.New("No InternetGatewayDevice")
   190  	}
   191  
   192  	dev, ok := getChildDevice(dev, "urn:schemas-upnp-org:device:WANDevice:1")
   193  	if !ok {
   194  		return "", "", errors.New("No WANDevice")
   195  	}
   196  
   197  	dev, ok = getChildDevice(dev, "urn:schemas-upnp-org:device:WANConnectionDevice:1")
   198  	if !ok {
   199  		return "", "", errors.New("No WANConnectionDevice")
   200  	}
   201  
   202  	device := "urn:schemas-upnp-org:service:WANIPConnection:1"
   203  	svc, ok := getChildService(dev, device)
   204  	if !ok {
   205  		device = "urn:schemas-upnp-org:service:WANPPPConnection:1"
   206  	}
   207  	svc, ok = getChildService(dev, device)
   208  	if !ok {
   209  		return "", "", errors.New("No WANIPConnection nor WANPPPConnection")
   210  	}
   211  
   212  	if len(svc.ControlURL) == 0 {
   213  		return "", "", errors.New("no controlURL")
   214  	}
   215  
   216  	u, _ := url.Parse(rootURL)
   217  	replaceRawPath(u, svc.ControlURL)
   218  	return u.String(), device, nil
   219  }
   220  
   221  func replaceRawPath(u *url.URL, rp string) {
   222  	var p, q string
   223  	fs := strings.Split(rp, "?")
   224  	p = fs[0]
   225  	if len(fs) > 1 {
   226  		q = fs[1]
   227  	}
   228  
   229  	if p[0] == '/' {
   230  		u.Path = p
   231  	} else {
   232  		u.Path += p
   233  	}
   234  	u.RawQuery = q
   235  }
   236  
   237  func soapRequest(url, device, function, message string) error {
   238  	tpl := `<?xml version="1.0" ?>
   239  	<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   240  	<s:Body>%s</s:Body>
   241  	</s:Envelope>
   242  `
   243  	body := fmt.Sprintf(tpl, message)
   244  
   245  	req, err := http.NewRequest("POST", url, strings.NewReader(body))
   246  	if err != nil {
   247  		return err
   248  	}
   249  	req.Header.Set("Content-Type", `text/xml; charset="utf-8"`)
   250  	req.Header.Set("User-Agent", "syncthing/1.0")
   251  	req.Header.Set("SOAPAction", fmt.Sprintf(`"%s#%s"`, device, function))
   252  	req.Header.Set("Connection", "Close")
   253  	req.Header.Set("Cache-Control", "no-cache")
   254  	req.Header.Set("Pragma", "no-cache")
   255  
   256  	if debug {
   257  		l.Debugln(req.Header.Get("SOAPAction"))
   258  		l.Debugln(body)
   259  	}
   260  
   261  	r, err := http.DefaultClient.Do(req)
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	if debug {
   267  		resp, _ := ioutil.ReadAll(r.Body)
   268  		l.Debugln(string(resp))
   269  	}
   270  
   271  	r.Body.Close()
   272  
   273  	if r.StatusCode >= 400 {
   274  		return errors.New(function + ": " + r.Status)
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
   281  	tpl := `<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
   282  	<NewRemoteHost></NewRemoteHost>
   283  	<NewExternalPort>%d</NewExternalPort>
   284  	<NewProtocol>%s</NewProtocol>
   285  	<NewInternalPort>%d</NewInternalPort>
   286  	<NewInternalClient>%s</NewInternalClient>
   287  	<NewEnabled>1</NewEnabled>
   288  	<NewPortMappingDescription>%s</NewPortMappingDescription>
   289  	<NewLeaseDuration>%d</NewLeaseDuration>
   290  	</u:AddPortMapping>
   291  	`
   292  
   293  	body := fmt.Sprintf(tpl, externalPort, protocol, internalPort, n.ourIP, description, timeout)
   294  	return soapRequest(n.serviceURL, n.device, "AddPortMapping", body)
   295  }
   296  
   297  func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) (err error) {
   298  	tpl := `<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
   299  	<NewRemoteHost></NewRemoteHost>
   300  	<NewExternalPort>%d</NewExternalPort>
   301  	<NewProtocol>%s</NewProtocol>
   302  	</u:DeletePortMapping>
   303  	`
   304  
   305  	body := fmt.Sprintf(tpl, externalPort, protocol)
   306  	return soapRequest(n.serviceURL, n.device, "DeletePortMapping", body)
   307  }