github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/p2p/upnp/upnp.go (about) 1 // Taken from taipei-torrent. 2 // Just enough UPnP to be able to forward ports 3 // For more information, see: http://www.upnp-hacks.org/upnp.html 4 package upnp 5 6 // TODO: use syscalls to get actual ourIP, see issue #712 7 8 import ( 9 "bytes" 10 "encoding/xml" 11 "errors" 12 "fmt" 13 "io" 14 "net" 15 "net/http" 16 "strconv" 17 "strings" 18 "time" 19 ) 20 21 type upnpNAT struct { 22 serviceURL string 23 ourIP string 24 urnDomain string 25 } 26 27 // protocol is either "udp" or "tcp" 28 type NAT interface { 29 GetExternalAddress() (addr net.IP, err error) 30 AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) 31 DeletePortMapping(protocol string, externalPort, internalPort int) (err error) 32 } 33 34 func Discover() (nat NAT, err error) { 35 ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") 36 if err != nil { 37 return 38 } 39 conn, err := net.ListenPacket("udp4", ":0") 40 if err != nil { 41 return 42 } 43 socket := conn.(*net.UDPConn) 44 defer socket.Close() //nolint: errcheck 45 46 if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil { 47 return nil, err 48 } 49 50 st := "InternetGatewayDevice:1" 51 52 buf := bytes.NewBufferString( 53 "M-SEARCH * HTTP/1.1\r\n" + 54 "HOST: 239.255.255.250:1900\r\n" + 55 "ST: ssdp:all\r\n" + 56 "MAN: \"ssdp:discover\"\r\n" + 57 "MX: 2\r\n\r\n") 58 message := buf.Bytes() 59 answerBytes := make([]byte, 1024) 60 for i := 0; i < 3; i++ { 61 _, err = socket.WriteToUDP(message, ssdp) 62 if err != nil { 63 return 64 } 65 var n int 66 _, _, err = socket.ReadFromUDP(answerBytes) 67 if err != nil { 68 return 69 } 70 for { 71 n, _, err = socket.ReadFromUDP(answerBytes) 72 if err != nil { 73 break 74 } 75 answer := string(answerBytes[0:n]) 76 if !strings.Contains(answer, st) { 77 continue 78 } 79 // HTTP header field names are case-insensitive. 80 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 81 locString := "\r\nlocation:" 82 answer = strings.ToLower(answer) 83 locIndex := strings.Index(answer, locString) 84 if locIndex < 0 { 85 continue 86 } 87 loc := answer[locIndex+len(locString):] 88 endIndex := strings.Index(loc, "\r\n") 89 if endIndex < 0 { 90 continue 91 } 92 locURL := strings.TrimSpace(loc[0:endIndex]) 93 var serviceURL, urnDomain string 94 serviceURL, urnDomain, err = getServiceURL(locURL) 95 if err != nil { 96 return 97 } 98 var ourIP net.IP 99 ourIP, err = localIPv4() 100 if err != nil { 101 return 102 } 103 nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain} 104 return 105 } 106 } 107 err = errors.New("UPnP port discovery failed") 108 return nat, err 109 } 110 111 type Envelope struct { 112 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 113 Soap *SoapBody 114 } 115 116 type SoapBody struct { 117 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` 118 ExternalIP *ExternalIPAddressResponse 119 } 120 121 type ExternalIPAddressResponse struct { 122 XMLName xml.Name `xml:"GetExternalIPAddressResponse"` 123 IPAddress string `xml:"NewExternalIPAddress"` 124 } 125 126 type ExternalIPAddress struct { 127 XMLName xml.Name `xml:"NewExternalIPAddress"` 128 IP string 129 } 130 131 type UPNPService struct { 132 ServiceType string `xml:"serviceType"` 133 ControlURL string `xml:"controlURL"` 134 } 135 136 type DeviceList struct { 137 Device []Device `xml:"device"` 138 } 139 140 type ServiceList struct { 141 Service []UPNPService `xml:"service"` 142 } 143 144 type Device struct { 145 XMLName xml.Name `xml:"device"` 146 DeviceType string `xml:"deviceType"` 147 DeviceList DeviceList `xml:"deviceList"` 148 ServiceList ServiceList `xml:"serviceList"` 149 } 150 151 type Root struct { 152 Device Device 153 } 154 155 func getChildDevice(d *Device, deviceType string) *Device { 156 dl := d.DeviceList.Device 157 for i := 0; i < len(dl); i++ { 158 if strings.Contains(dl[i].DeviceType, deviceType) { 159 return &dl[i] 160 } 161 } 162 return nil 163 } 164 165 func getChildService(d *Device, serviceType string) *UPNPService { 166 sl := d.ServiceList.Service 167 for i := 0; i < len(sl); i++ { 168 if strings.Contains(sl[i].ServiceType, serviceType) { 169 return &sl[i] 170 } 171 } 172 return nil 173 } 174 175 func localIPv4() (net.IP, error) { 176 tt, err := net.Interfaces() 177 if err != nil { 178 return nil, err 179 } 180 for _, t := range tt { 181 aa, err := t.Addrs() 182 if err != nil { 183 return nil, err 184 } 185 for _, a := range aa { 186 ipnet, ok := a.(*net.IPNet) 187 if !ok { 188 continue 189 } 190 v4 := ipnet.IP.To4() 191 if v4 == nil || v4[0] == 127 { // loopback address 192 continue 193 } 194 return v4, nil 195 } 196 } 197 return nil, errors.New("cannot find local IP address") 198 } 199 200 func getServiceURL(rootURL string) (url, urnDomain string, err error) { 201 r, err := http.Get(rootURL) //nolint: gosec 202 if err != nil { 203 return 204 } 205 defer r.Body.Close() //nolint: errcheck 206 207 if r.StatusCode >= 400 { 208 err = errors.New(fmt.Sprint(r.StatusCode)) 209 return 210 } 211 var root Root 212 err = xml.NewDecoder(r.Body).Decode(&root) 213 if err != nil { 214 return 215 } 216 a := &root.Device 217 if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") { 218 err = errors.New("no InternetGatewayDevice") 219 return 220 } 221 b := getChildDevice(a, "WANDevice:1") 222 if b == nil { 223 err = errors.New("no WANDevice") 224 return 225 } 226 c := getChildDevice(b, "WANConnectionDevice:1") 227 if c == nil { 228 err = errors.New("no WANConnectionDevice") 229 return 230 } 231 d := getChildService(c, "WANIPConnection:1") 232 if d == nil { 233 // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice, 234 // instead of under WanConnectionDevice 235 d = getChildService(b, "WANIPConnection:1") 236 237 if d == nil { 238 err = errors.New("no WANIPConnection") 239 return 240 } 241 } 242 // Extract the domain name, which isn't always 'schemas-upnp-org' 243 urnDomain = strings.Split(d.ServiceType, ":")[1] 244 url = combineURL(rootURL, d.ControlURL) 245 return url, urnDomain, err 246 } 247 248 func combineURL(rootURL, subURL string) string { 249 protocolEnd := "://" 250 protoEndIndex := strings.Index(rootURL, protocolEnd) 251 a := rootURL[protoEndIndex+len(protocolEnd):] 252 rootIndex := strings.Index(a, "/") 253 return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL 254 } 255 256 func soapRequest(url, function, message, domain string) (r *http.Response, err error) { 257 fullMessage := "<?xml version=\"1.0\" ?>" + 258 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" + 259 "<s:Body>" + message + "</s:Body></s:Envelope>" 260 261 req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) 262 if err != nil { 263 return nil, err 264 } 265 req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") 266 req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") 267 // req.Header.Set("Transfer-Encoding", "chunked") 268 req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"") 269 req.Header.Set("Connection", "Close") 270 req.Header.Set("Cache-Control", "no-cache") 271 req.Header.Set("Pragma", "no-cache") 272 273 // log.Stderr("soapRequest ", req) 274 275 r, err = http.DefaultClient.Do(req) 276 if err != nil { 277 return nil, err 278 } 279 /*if r.Body != nil { 280 defer r.Body.Close() 281 }*/ 282 283 if r.StatusCode >= 400 { 284 // log.Stderr(function, r.StatusCode) 285 err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) 286 r = nil 287 return 288 } 289 return r, err 290 } 291 292 type statusInfo struct { 293 externalIPAddress string 294 } 295 296 func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { 297 message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 298 "</u:GetExternalIPAddress>" 299 300 var response *http.Response 301 response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) 302 if response != nil { 303 defer response.Body.Close() //nolint: errcheck 304 } 305 if err != nil { 306 return 307 } 308 var envelope Envelope 309 data, err := io.ReadAll(response.Body) 310 if err != nil { 311 return 312 } 313 reader := bytes.NewReader(data) 314 err = xml.NewDecoder(reader).Decode(&envelope) 315 if err != nil { 316 return 317 } 318 319 info = statusInfo{envelope.Soap.ExternalIP.IPAddress} 320 321 if err != nil { 322 return 323 } 324 325 return info, err 326 } 327 328 // GetExternalAddress returns an external IP. If GetExternalIPAddress action 329 // fails or IP returned is invalid, GetExternalAddress returns an error. 330 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { 331 info, err := n.getExternalIPAddress() 332 if err != nil { 333 return 334 } 335 addr = net.ParseIP(info.externalIPAddress) 336 if addr == nil { 337 err = fmt.Errorf("failed to parse IP: %v", info.externalIPAddress) 338 } 339 return 340 } 341 342 func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { 343 // A single concatenation would break ARM compilation. 344 message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 345 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) 346 message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" 347 message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" + 348 "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" + 349 "<NewEnabled>1</NewEnabled><NewPortMappingDescription>" 350 message += description + 351 "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) + 352 "</NewLeaseDuration></u:AddPortMapping>" 353 354 var response *http.Response 355 response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) 356 if response != nil { 357 defer response.Body.Close() //nolint: errcheck 358 } 359 if err != nil { 360 return 361 } 362 363 // TODO: check response to see if the port was forwarded 364 // log.Println(message, response) 365 // JAE: 366 // body, err := io.ReadAll(response.Body) 367 // fmt.Println(string(body), err) 368 mappedExternalPort = externalPort 369 _ = response 370 return 371 } 372 373 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { 374 message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + 375 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) + 376 "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" + 377 "</u:DeletePortMapping>" 378 379 var response *http.Response 380 response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) 381 if response != nil { 382 defer response.Body.Close() //nolint: errcheck 383 } 384 if err != nil { 385 return 386 } 387 388 // TODO: check response to see if the port was deleted 389 // log.Println(message, response) 390 _ = response 391 return 392 }