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 }