- // Taken from taipei-torrent.
- // Just enough UPnP to be able to forward ports
- // For more information, see: http://www.upnp-hacks.org/upnp.html
- package upnp
-
- // TODO: use syscalls to get actual ourIP, see issue #712
-
- import (
- "bytes"
- "encoding/xml"
- "errors"
- "fmt"
- "io/ioutil"
- "net"
- "net/http"
- "strconv"
- "strings"
- "time"
- )
-
- type upnpNAT struct {
- serviceURL string
- ourIP string
- urnDomain string
- }
-
- // protocol is either "udp" or "tcp"
- type NAT interface {
- GetExternalAddress() (addr net.IP, err error)
- AddPortMapping(
- protocol string,
- externalPort,
- internalPort int,
- description string,
- timeout int) (mappedExternalPort int, err error)
- DeletePortMapping(protocol string, externalPort, internalPort int) (err error)
- }
-
- func Discover() (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()
-
- if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
- return nil, err
- }
-
- st := "InternetGatewayDevice:1"
-
- buf := bytes.NewBufferString(
- "M-SEARCH * HTTP/1.1\r\n" +
- "HOST: 239.255.255.250:1900\r\n" +
- "ST: ssdp:all\r\n" +
- "MAN: \"ssdp:discover\"\r\n" +
- "MX: 2\r\n\r\n")
- message := buf.Bytes()
- answerBytes := make([]byte, 1024)
- for i := 0; i < 3; i++ {
- _, err = socket.WriteToUDP(message, ssdp)
- if err != nil {
- return
- }
- var n int
- _, _, err = socket.ReadFromUDP(answerBytes)
- if err != nil {
- return
- }
- for {
- n, _, err = socket.ReadFromUDP(answerBytes)
- if err != nil {
- break
- }
- answer := string(answerBytes[0:n])
- if !strings.Contains(answer, st) {
- 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 := strings.TrimSpace(loc[0:endIndex])
- var serviceURL, urnDomain string
- serviceURL, urnDomain, err = getServiceURL(locURL)
- if err != nil {
- return
- }
- var ourIP net.IP
- ourIP, err = localIPv4()
- if err != nil {
- return
- }
- nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain}
- return
- }
- }
- err = errors.New("upnp port discovery failed")
- return nat, err
- }
-
- type Envelope struct {
- XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
- Soap *SoapBody
- }
- type SoapBody struct {
- XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
- ExternalIP *ExternalIPAddressResponse
- }
-
- type ExternalIPAddressResponse struct {
- XMLName xml.Name `xml:"GetExternalIPAddressResponse"`
- IPAddress string `xml:"NewExternalIPAddress"`
- }
-
- type ExternalIPAddress struct {
- XMLName xml.Name `xml:"NewExternalIPAddress"`
- IP string
- }
-
- type Service struct {
- ServiceType string `xml:"serviceType"`
- ControlURL string `xml:"controlURL"`
- }
-
- type DeviceList struct {
- Device []Device `xml:"device"`
- }
-
- type ServiceList struct {
- Service []Service `xml:"service"`
- }
-
- type Device struct {
- XMLName xml.Name `xml:"device"`
- DeviceType string `xml:"deviceType"`
- DeviceList DeviceList `xml:"deviceList"`
- ServiceList ServiceList `xml:"serviceList"`
- }
-
- type Root struct {
- Device Device
- }
-
- func getChildDevice(d *Device, deviceType string) *Device {
- dl := d.DeviceList.Device
- for i := 0; i < len(dl); i++ {
- if strings.Contains(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 strings.Contains(sl[i].ServiceType, serviceType) {
- return &sl[i]
- }
- }
- return nil
- }
-
- func localIPv4() (net.IP, error) {
- tt, err := net.Interfaces()
- if err != nil {
- return nil, err
- }
- for _, t := range tt {
- aa, err := t.Addrs()
- if err != nil {
- return nil, err
- }
- for _, a := range aa {
- ipnet, ok := a.(*net.IPNet)
- if !ok {
- continue
- }
- v4 := ipnet.IP.To4()
- if v4 == nil || v4[0] == 127 { // loopback address
- continue
- }
- return v4, nil
- }
- }
- return nil, errors.New("cannot find local IP address")
- }
-
- func getServiceURL(rootURL string) (url, urnDomain string, err error) {
- r, err := http.Get(rootURL) // nolint: gosec
- if err != nil {
- return
- }
- defer r.Body.Close()
-
- if r.StatusCode >= 400 {
- err = errors.New(string(rune(r.StatusCode)))
- return
- }
- var root Root
- err = xml.NewDecoder(r.Body).Decode(&root)
- if err != nil {
- return
- }
- a := &root.Device
- if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") {
- err = errors.New("no InternetGatewayDevice")
- return
- }
- b := getChildDevice(a, "WANDevice:1")
- if b == nil {
- err = errors.New("no WANDevice")
- return
- }
- c := getChildDevice(b, "WANConnectionDevice:1")
- if c == nil {
- err = errors.New("no WANConnectionDevice")
- return
- }
- d := getChildService(c, "WANIPConnection:1")
- if d == nil {
- // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice,
- // instead of under WanConnectionDevice
- d = getChildService(b, "WANIPConnection:1")
-
- if d == nil {
- err = errors.New("no WANIPConnection")
- return
- }
- }
- // Extract the domain name, which isn't always 'schemas-upnp-org'
- urnDomain = strings.Split(d.ServiceType, ":")[1]
- url = combineURL(rootURL, d.ControlURL)
- return url, urnDomain, err
- }
-
- 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, domain 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 nil, err
- }
- 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:"+domain+":service:WANIPConnection:1#"+function+"\"")
- req.Header.Set("Connection", "Close")
- req.Header.Set("Cache-Control", "no-cache")
- req.Header.Set("Pragma", "no-cache")
-
- // log.Stderr("soapRequest ", req)
-
- r, err = http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- /*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 r, err
- }
-
- type statusInfo struct {
- externalIPAddress string
- }
-
- func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) {
-
- message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
- "</u:GetExternalIPAddress>"
-
- var response *http.Response
- response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain)
- if response != nil {
- defer response.Body.Close()
- }
- if err != nil {
- return
- }
- var envelope Envelope
- data, err := ioutil.ReadAll(response.Body)
- if err != nil {
- return
- }
- reader := bytes.NewReader(data)
- err = xml.NewDecoder(reader).Decode(&envelope)
- if err != nil {
- return
- }
-
- info = statusInfo{envelope.Soap.ExternalIP.IPAddress}
-
- if err != nil {
- return
- }
-
- return info, err
- }
-
- // GetExternalAddress returns an external IP. If GetExternalIPAddress action
- // fails or IP returned is invalid, GetExternalAddress returns an error.
- func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
- info, err := n.getExternalIPAddress()
- if err != nil {
- return
- }
- addr = net.ParseIP(info.externalIPAddress)
- if addr == nil {
- err = fmt.Errorf("failed to parse IP: %v", 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:" + n.urnDomain + ":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, n.urnDomain)
- if response != nil {
- defer response.Body.Close()
- }
- if err != nil {
- return
- }
-
- // TODO: check response to see if the port was forwarded
- // log.Println(message, response)
- // JAE:
- // body, err := ioutil.ReadAll(response.Body)
- // fmt.Println(string(body), err)
- mappedExternalPort = externalPort
- _ = response
- return mappedExternalPort, err
- }
-
- func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {
-
- message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":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, n.urnDomain)
- if response != nil {
- defer response.Body.Close()
- }
- if err != nil {
- return
- }
-
- // TODO: check response to see if the port was deleted
- // log.Println(message, response)
- _ = response
- return
- }
|