aboutsummaryrefslogblamecommitdiffstats
path: root/p2p/natupnp.go
blob: fa9798d4d575e023010cd04a4765d0b60487af2b (plain) (tree)














































































































































































































































































































































                                                                                                                                                        
package p2p

// Just enough UPnP to be able to forward ports
//

import (
    "bytes"
    "encoding/xml"
    "errors"
    "net"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type upnpNAT struct {
    serviceURL string
    ourIP      string
}

func upnpDiscover(attempts int) (nat NAT, err error) {
    ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900")
    if err != nil {
        return
    }
    conn, err := net.ListenPacket("udp4", ":0")
    if err != nil {
        return
    }
    socket := conn.(*net.UDPConn)
    defer socket.Close()

    err = socket.SetDeadline(time.Now().Add(10 * time.Second))
    if err != nil {
        return
    }

    st := "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n"
    buf := bytes.NewBufferString(
        "M-SEARCH * HTTP/1.1\r\n" +
            "HOST: 239.255.255.250:1900\r\n" +
            st +
            "MAN: \"ssdp:discover\"\r\n" +
            "MX: 2\r\n\r\n")
    message := buf.Bytes()
    answerBytes := make([]byte, 1024)
    for i := 0; i < attempts; i++ {
        _, err = socket.WriteToUDP(message, ssdp)
        if err != nil {
            return
        }
        var n int
        n, _, err = socket.ReadFromUDP(answerBytes)
        if err != nil {
            continue
            // socket.Close()
            // return
        }
        answer := string(answerBytes[0:n])
        if strings.Index(answer, "\r\n"+st) < 0 {
            continue
        }
        // HTTP header field names are case-insensitive.
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
        locString := "\r\nlocation: "
        answer = strings.ToLower(answer)
        locIndex := strings.Index(answer, locString)
        if locIndex < 0 {
            continue
        }
        loc := answer[locIndex+len(locString):]
        endIndex := strings.Index(loc, "\r\n")
        if endIndex < 0 {
            continue
        }
        locURL := loc[0:endIndex]
        var serviceURL string
        serviceURL, err = getServiceURL(locURL)
        if err != nil {
            return
        }
        var ourIP string
        ourIP, err = getOurIP()
        if err != nil {
            return
        }
        nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP}
        return
    }
    err = errors.New("UPnP port discovery failed.")
    return
}

// service represents the Service type in an UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type service struct {
    ServiceType string `xml:"serviceType"`
    ControlURL  string `xml:"controlURL"`
}

// deviceList represents the deviceList type in an UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type deviceList struct {
    XMLName xml.Name `xml:"deviceList"`
    Device  []device `xml:"device"`
}

// serviceList represents the serviceList type in an UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type serviceList struct {
    XMLName xml.Name  `xml:"serviceList"`
    Service []service `xml:"service"`
}

// device represents the device type in an UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type device struct {
    XMLName     xml.Name    `xml:"device"`
    DeviceType  string      `xml:"deviceType"`
    DeviceList  deviceList  `xml:"deviceList"`
    ServiceList serviceList `xml:"serviceList"`
}

// specVersion represents the specVersion in a UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type specVersion struct {
    XMLName xml.Name `xml:"specVersion"`
    Major   int      `xml:"major"`
    Minor   int      `xml:"minor"`
}

// root represents the Root document for a UPnP xml description.
// Only the parts we care about are present and thus the xml may have more
// fields than present in the structure.
type root struct {
    XMLName     xml.Name `xml:"root"`
    SpecVersion specVersion
    Device      device
}

func getChildDevice(d *device, deviceType string) *device {
    dl := d.DeviceList.Device
    for i := 0; i < len(dl); i++ {
        if dl[i].DeviceType == deviceType {
            return &dl[i]
        }
    }
    return nil
}

func getChildService(d *device, serviceType string) *service {
    sl := d.ServiceList.Service
    for i := 0; i < len(sl); i++ {
        if sl[i].ServiceType == serviceType {
            return &sl[i]
        }
    }
    return nil
}

func getOurIP() (ip string, err error) {
    hostname, err := os.Hostname()
    if err != nil {
        return
    }
    p, err := net.LookupIP(hostname)
    if err != nil && len(p) > 0 {
        return
    }
    return p[0].String(), nil
}

func getServiceURL(rootURL string) (url string, err error) {
    r, err := http.Get(rootURL)
    if err != nil {
        return
    }
    defer r.Body.Close()
    if r.StatusCode >= 400 {
        err = errors.New(string(r.StatusCode))
        return
    }
    var root root
    err = xml.NewDecoder(r.Body).Decode(&root)

    if err != nil {
        return
    }
    a := &root.Device
    if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
        err = errors.New("No InternetGatewayDevice")
        return
    }
    b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1")
    if b == nil {
        err = errors.New("No WANDevice")
        return
    }
    c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1")
    if c == nil {
        err = errors.New("No WANConnectionDevice")
        return
    }
    d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1")
    if d == nil {
        err = errors.New("No WANIPConnection")
        return
    }
    url = combineURL(rootURL, d.ControlURL)
    return
}

func combineURL(rootURL, subURL string) string {
    protocolEnd := "://"
    protoEndIndex := strings.Index(rootURL, protocolEnd)
    a := rootURL[protoEndIndex+len(protocolEnd):]
    rootIndex := strings.Index(a, "/")
    return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL
}

func soapRequest(url, function, message string) (r *http.Response, err error) {
    fullMessage := "<?xml version=\"1.0\" ?>" +
        "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" +
        "<s:Body>" + message + "</s:Body></s:Envelope>"

    req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage))
    if err != nil {
        return
    }
    req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"")
    req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3")
    //req.Header.Set("Transfer-Encoding", "chunked")
    req.Header.Set("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#"+function+"\"")
    req.Header.Set("Connection", "Close")
    req.Header.Set("Cache-Control", "no-cache")
    req.Header.Set("Pragma", "no-cache")

    r, err = http.DefaultClient.Do(req)
    if err != nil {
        return
    }

    if r.Body != nil {
        defer r.Body.Close()
    }

    if r.StatusCode >= 400 {
        // log.Stderr(function, r.StatusCode)
        err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function)
        r = nil
        return
    }
    return
}

type statusInfo struct {
    externalIpAddress string
}

func (n *upnpNAT) getStatusInfo() (info statusInfo, err error) {

    message := "<u:GetStatusInfo xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" +
        "</u:GetStatusInfo>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "GetStatusInfo", message)
    if err != nil {
        return
    }

    // TODO: Write a soap reply parser. It has to eat the Body and envelope tags...

    response.Body.Close()
    return
}

func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
    info, err := n.getStatusInfo()
    if err != nil {
        return
    }
    addr = net.ParseIP(info.externalIpAddress)
    return
}

func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) {
    // A single concatenation would break ARM compilation.
    message := "<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" +
        "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
    message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>"
    message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
        "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" +
        "<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
    message += description +
        "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
        "</NewLeaseDuration></u:AddPortMapping>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "AddPortMapping", message)
    if err != nil {
        return
    }

    // TODO: check response to see if the port was forwarded
    // log.Println(message, response)
    mappedExternalPort = externalPort
    _ = response
    return
}

func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {

    message := "<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" +
        "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
        "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" +
        "</u:DeletePortMapping>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "DeletePortMapping", message)
    if err != nil {
        return
    }

    // TODO: check response to see if the port was deleted
    // log.Println(message, response)
    _ = response
    return
}