github.com/lbryio/lbcd@v0.22.119/upnp.go (about) 1 package main 2 3 // Upnp code taken from Taipei Torrent license is below: 4 // Copyright (c) 2010 Jack Palevich. All rights reserved. 5 // 6 // Redistribution and use in source and binary forms, with or without 7 // modification, are permitted provided that the following conditions are 8 // met: 9 // 10 // * Redistributions of source code must retain the above copyright 11 // notice, this list of conditions and the following disclaimer. 12 // * Redistributions in binary form must reproduce the above 13 // copyright notice, this list of conditions and the following disclaimer 14 // in the documentation and/or other materials provided with the 15 // distribution. 16 // * Neither the name of Google Inc. nor the names of its 17 // contributors may be used to endorse or promote products derived from 18 // this software without specific prior written permission. 19 // 20 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 32 // Just enough UPnP to be able to forward ports 33 // 34 35 import ( 36 "bytes" 37 "encoding/xml" 38 "errors" 39 "fmt" 40 "net" 41 "net/http" 42 "net/url" 43 "strconv" 44 "strings" 45 "time" 46 ) 47 48 // NAT is an interface representing a NAT traversal options for example UPNP or 49 // NAT-PMP. It provides methods to query and manipulate this traversal to allow 50 // access to services. 51 type NAT interface { 52 // Get the external address from outside the NAT. 53 GetExternalAddress() (addr net.IP, err error) 54 // Add a port mapping for protocol ("udp" or "tcp") from external port to 55 // internal port with description lasting for timeout. 56 AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) 57 // Remove a previously added port mapping from external port to 58 // internal port. 59 DeletePortMapping(protocol string, externalPort, internalPort int) (err error) 60 } 61 62 type upnpNAT struct { 63 serviceURL string 64 ourIP string 65 } 66 67 // Discover searches the local network for a UPnP router returning a NAT 68 // for the network if so, nil if not. 69 func Discover() (nat NAT, err error) { 70 ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") 71 if err != nil { 72 return 73 } 74 conn, err := net.ListenPacket("udp4", ":0") 75 if err != nil { 76 return 77 } 78 socket := conn.(*net.UDPConn) 79 defer socket.Close() 80 81 err = socket.SetDeadline(time.Now().Add(3 * time.Second)) 82 if err != nil { 83 return 84 } 85 86 st := "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" 87 buf := bytes.NewBufferString( 88 "M-SEARCH * HTTP/1.1\r\n" + 89 "HOST: 239.255.255.250:1900\r\n" + 90 st + 91 "MAN: \"ssdp:discover\"\r\n" + 92 "MX: 2\r\n\r\n") 93 message := buf.Bytes() 94 answerBytes := make([]byte, 1024) 95 for i := 0; i < 3; i++ { 96 _, err = socket.WriteToUDP(message, ssdp) 97 if err != nil { 98 return 99 } 100 var n int 101 n, _, err = socket.ReadFromUDP(answerBytes) 102 if err != nil { 103 continue 104 // socket.Close() 105 // return 106 } 107 answer := string(answerBytes[0:n]) 108 if !strings.Contains(answer, "\r\n"+st) { 109 continue 110 } 111 // HTTP header field names are case-insensitive. 112 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 113 locString := "\r\nlocation: " 114 locIndex := strings.Index(strings.ToLower(answer), locString) 115 if locIndex < 0 { 116 continue 117 } 118 loc := answer[locIndex+len(locString):] 119 endIndex := strings.Index(loc, "\r\n") 120 if endIndex < 0 { 121 continue 122 } 123 locURL := loc[0:endIndex] 124 var serviceURL string 125 serviceURL, err = getServiceURL(locURL) 126 if err != nil { 127 return 128 } 129 var serviceIP string = getServiceIP(serviceURL) 130 var ourIP string 131 ourIP, err = getOurIP(serviceIP) 132 if err != nil { 133 return 134 } 135 nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP} 136 return 137 } 138 err = errors.New("UPnP port discovery failed") 139 return nat, err 140 } 141 142 // service represents the Service type in an UPnP xml description. 143 // Only the parts we care about are present and thus the xml may have more 144 // fields than present in the structure. 145 type service struct { 146 ServiceType string `xml:"serviceType"` 147 ControlURL string `xml:"controlURL"` 148 } 149 150 // deviceList represents the deviceList type in an UPnP xml description. 151 // Only the parts we care about are present and thus the xml may have more 152 // fields than present in the structure. 153 type deviceList struct { 154 XMLName xml.Name `xml:"deviceList"` 155 Device []device `xml:"device"` 156 } 157 158 // serviceList represents the serviceList type in an UPnP xml description. 159 // Only the parts we care about are present and thus the xml may have more 160 // fields than present in the structure. 161 type serviceList struct { 162 XMLName xml.Name `xml:"serviceList"` 163 Service []service `xml:"service"` 164 } 165 166 // device represents the device type in an UPnP xml description. 167 // Only the parts we care about are present and thus the xml may have more 168 // fields than present in the structure. 169 type device struct { 170 XMLName xml.Name `xml:"device"` 171 DeviceType string `xml:"deviceType"` 172 DeviceList deviceList `xml:"deviceList"` 173 ServiceList serviceList `xml:"serviceList"` 174 } 175 176 // specVersion represents the specVersion in a UPnP xml description. 177 // Only the parts we care about are present and thus the xml may have more 178 // fields than present in the structure. 179 type specVersion struct { 180 XMLName xml.Name `xml:"specVersion"` 181 Major int `xml:"major"` 182 Minor int `xml:"minor"` 183 } 184 185 // root represents the Root document for a UPnP xml description. 186 // Only the parts we care about are present and thus the xml may have more 187 // fields than present in the structure. 188 type root struct { 189 XMLName xml.Name `xml:"root"` 190 SpecVersion specVersion 191 Device device 192 } 193 194 // getChildDevice searches the children of device for a device with the given 195 // type. 196 func getChildDevice(d *device, deviceType string) *device { 197 for i := range d.DeviceList.Device { 198 if d.DeviceList.Device[i].DeviceType == deviceType { 199 return &d.DeviceList.Device[i] 200 } 201 } 202 return nil 203 } 204 205 // getChildDevice searches the service list of device for a service with the 206 // given type. 207 func getChildService(d *device, serviceType string) *service { 208 for i := range d.ServiceList.Service { 209 if d.ServiceList.Service[i].ServiceType == serviceType { 210 return &d.ServiceList.Service[i] 211 } 212 } 213 return nil 214 } 215 216 func getServiceIP(serviceURL string) (routerIP string) { 217 url, _ := url.Parse(serviceURL) 218 return url.Hostname() 219 } 220 221 // getOurIP returns the local IP that is on the same subnet as the serviceIP. 222 func getOurIP(serviceIP string) (ip string, err error) { 223 _, serviceNet, _ := net.ParseCIDR(serviceIP + "/24") 224 addrs, err := net.InterfaceAddrs() 225 for _, addr := range addrs { 226 ip, _, _ := net.ParseCIDR(addr.String()) 227 if serviceNet.Contains(ip) { 228 return ip.String(), nil 229 } 230 } 231 return 232 } 233 234 // getServiceURL parses the xml description at the given root url to find the 235 // url for the WANIPConnection service to be used for port forwarding. 236 func getServiceURL(rootURL string) (url string, err error) { 237 r, err := http.Get(rootURL) 238 if err != nil { 239 return 240 } 241 defer r.Body.Close() 242 if r.StatusCode >= 400 { 243 err = errors.New(fmt.Sprint(r.StatusCode)) 244 return 245 } 246 var root root 247 err = xml.NewDecoder(r.Body).Decode(&root) 248 if err != nil { 249 return 250 } 251 a := &root.Device 252 if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" { 253 err = errors.New("no InternetGatewayDevice") 254 return 255 } 256 b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1") 257 if b == nil { 258 err = errors.New("no WANDevice") 259 return 260 } 261 c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1") 262 if c == nil { 263 err = errors.New("no WANConnectionDevice") 264 return 265 } 266 d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1") 267 if d == nil { 268 err = errors.New("no WANIPConnection") 269 return 270 } 271 url = combineURL(rootURL, d.ControlURL) 272 return url, err 273 } 274 275 // combineURL appends subURL onto rootURL. 276 func combineURL(rootURL, subURL string) string { 277 protocolEnd := "://" 278 protoEndIndex := strings.Index(rootURL, protocolEnd) 279 a := rootURL[protoEndIndex+len(protocolEnd):] 280 rootIndex := strings.Index(a, "/") 281 return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL 282 } 283 284 // soapBody represents the <s:Body> element in a SOAP reply. 285 // fields we don't care about are elided. 286 type soapBody struct { 287 XMLName xml.Name `xml:"Body"` 288 Data []byte `xml:",innerxml"` 289 } 290 291 // soapEnvelope represents the <s:Envelope> element in a SOAP reply. 292 // fields we don't care about are elided. 293 type soapEnvelope struct { 294 XMLName xml.Name `xml:"Envelope"` 295 Body soapBody `xml:"Body"` 296 } 297 298 // soapRequests performs a soap request with the given parameters and returns 299 // the xml replied stripped of the soap headers. in the case that the request is 300 // unsuccessful the an error is returned. 301 func soapRequest(url, function, message string) (replyXML []byte, err error) { 302 fullMessage := "<?xml version=\"1.0\" ?>" + 303 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" + 304 "<s:Body>" + message + "</s:Body></s:Envelope>" 305 306 req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) 307 if err != nil { 308 return nil, err 309 } 310 req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") 311 req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") 312 req.Header.Set("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#"+function+"\"") 313 req.Header.Set("Connection", "Close") 314 req.Header.Set("Cache-Control", "no-cache") 315 req.Header.Set("Pragma", "no-cache") 316 317 r, err := http.DefaultClient.Do(req) 318 if err != nil { 319 return nil, err 320 } 321 if r.Body != nil { 322 defer r.Body.Close() 323 } 324 325 if r.StatusCode >= 400 { 326 err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) 327 r = nil 328 return 329 } 330 var reply soapEnvelope 331 err = xml.NewDecoder(r.Body).Decode(&reply) 332 if err != nil { 333 return nil, err 334 } 335 return reply.Body.Data, nil 336 } 337 338 // getExternalIPAddressResponse represents the XML response to a 339 // GetExternalIPAddress SOAP request. 340 type getExternalIPAddressResponse struct { 341 XMLName xml.Name `xml:"GetExternalIPAddressResponse"` 342 ExternalIPAddress string `xml:"NewExternalIPAddress"` 343 } 344 345 // GetExternalAddress implements the NAT interface by fetching the external IP 346 // from the UPnP router. 347 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { 348 message := "<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>\r\n" 349 response, err := soapRequest(n.serviceURL, "GetExternalIPAddress", message) 350 if err != nil { 351 return nil, err 352 } 353 354 var reply getExternalIPAddressResponse 355 err = xml.Unmarshal(response, &reply) 356 if err != nil { 357 return nil, err 358 } 359 360 addr = net.ParseIP(reply.ExternalIPAddress) 361 if addr == nil { 362 return nil, errors.New("unable to parse ip address") 363 } 364 return addr, nil 365 } 366 367 // AddPortMapping implements the NAT interface by setting up a port forwarding 368 // from the UPnP router to the local machine with the given ports and protocol. 369 func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { 370 // A single concatenation would break ARM compilation. 371 message := "<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" + 372 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) 373 message += "</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>" 374 message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" + 375 "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" + 376 "<NewEnabled>1</NewEnabled><NewPortMappingDescription>" 377 message += description + 378 "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) + 379 "</NewLeaseDuration></u:AddPortMapping>" 380 381 response, err := soapRequest(n.serviceURL, "AddPortMapping", message) 382 if err != nil { 383 return 384 } 385 386 // TODO: check response to see if the port was forwarded 387 // If the port was not wildcard we don't get an reply with the port in 388 // it. Not sure about wildcard yet. miniupnpc just checks for error 389 // codes here. 390 mappedExternalPort = externalPort 391 _ = response 392 return 393 } 394 395 // DeletePortMapping implements the NAT interface by removing up a port forwarding 396 // from the UPnP router to the local machine with the given ports and. 397 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { 398 399 message := "<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">\r\n" + 400 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) + 401 "</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>" + 402 "</u:DeletePortMapping>" 403 404 response, err := soapRequest(n.serviceURL, "DeletePortMapping", message) 405 if err != nil { 406 return 407 } 408 409 // TODO: check response to see if the port was deleted 410 // log.Println(message, response) 411 _ = response 412 return 413 }