2 Taken from taipei-torrent
4 Just enough UPnP to be able to forward ports
8 // BUG(jae): TODO: use syscalls to get actual ourIP. http://pastebin.com/9exZG4rh
29 // protocol is either "udp" or "tcp"
31 GetExternalAddress() (addr net.IP, err error)
32 AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error)
33 DeletePortMapping(protocol string, externalPort, internalPort int) (err error)
36 func Discover() (nat NAT, err error) {
37 ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900")
41 conn, err := net.ListenPacket("udp4", ":0")
45 socket := conn.(*net.UDPConn)
46 defer socket.Close() // nolint: errcheck
48 if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
52 st := "InternetGatewayDevice:1"
54 buf := bytes.NewBufferString(
55 "M-SEARCH * HTTP/1.1\r\n" +
56 "HOST: 239.255.255.250:1900\r\n" +
58 "MAN: \"ssdp:discover\"\r\n" +
60 message := buf.Bytes()
61 answerBytes := make([]byte, 1024)
62 for i := 0; i < 3; i++ {
63 _, err = socket.WriteToUDP(message, ssdp)
68 _, _, err = socket.ReadFromUDP(answerBytes)
74 n, _, err = socket.ReadFromUDP(answerBytes)
78 answer := string(answerBytes[0:n])
79 if !strings.Contains(answer, st) {
82 // HTTP header field names are case-insensitive.
83 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
84 locString := "\r\nlocation:"
85 answer = strings.ToLower(answer)
86 locIndex := strings.Index(answer, locString)
90 loc := answer[locIndex+len(locString):]
91 endIndex := strings.Index(loc, "\r\n")
95 locURL := strings.TrimSpace(loc[0:endIndex])
96 var serviceURL, urnDomain string
97 serviceURL, urnDomain, err = getServiceURL(locURL)
102 ourIP, err = localIPv4()
106 nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain}
110 err = errors.New("UPnP port discovery failed")
114 type Envelope struct {
115 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
118 type SoapBody struct {
119 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
120 ExternalIP *ExternalIPAddressResponse
123 type ExternalIPAddressResponse struct {
124 XMLName xml.Name `xml:"GetExternalIPAddressResponse"`
125 IPAddress string `xml:"NewExternalIPAddress"`
128 type ExternalIPAddress struct {
129 XMLName xml.Name `xml:"NewExternalIPAddress"`
133 type UPNPService struct {
134 ServiceType string `xml:"serviceType"`
135 ControlURL string `xml:"controlURL"`
138 type DeviceList struct {
139 Device []Device `xml:"device"`
142 type ServiceList struct {
143 Service []UPNPService `xml:"service"`
147 XMLName xml.Name `xml:"device"`
148 DeviceType string `xml:"deviceType"`
149 DeviceList DeviceList `xml:"deviceList"`
150 ServiceList ServiceList `xml:"serviceList"`
157 func getChildDevice(d *Device, deviceType string) *Device {
158 dl := d.DeviceList.Device
159 for i := 0; i < len(dl); i++ {
160 if strings.Contains(dl[i].DeviceType, deviceType) {
167 func getChildService(d *Device, serviceType string) *UPNPService {
168 sl := d.ServiceList.Service
169 for i := 0; i < len(sl); i++ {
170 if strings.Contains(sl[i].ServiceType, serviceType) {
177 func localIPv4() (net.IP, error) {
178 tt, err := net.Interfaces()
182 for _, t := range tt {
187 for _, a := range aa {
188 ipnet, ok := a.(*net.IPNet)
193 if v4 == nil || v4[0] == 127 { // loopback address
199 return nil, errors.New("cannot find local IP address")
202 func getServiceURL(rootURL string) (url, urnDomain string, err error) {
203 r, err := http.Get(rootURL)
207 defer r.Body.Close() // nolint: errcheck
209 if r.StatusCode >= 400 {
210 err = errors.New(string(r.StatusCode))
214 err = xml.NewDecoder(r.Body).Decode(&root)
219 if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") {
220 err = errors.New("No InternetGatewayDevice")
224 b := getChildDevice(a, "WANDevice:1")
226 err = errors.New("No WANDevice")
229 c := getChildDevice(b, "WANConnectionDevice:1")
231 err = errors.New("No WANConnectionDevice")
234 d := getChildService(c, "WANIPConnection:1")
236 // Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice,
237 // instead of under WanConnectionDevice
238 d = getChildService(b, "WANIPConnection:1")
241 err = errors.New("No WANIPConnection")
245 // Extract the domain name, which isn't always 'schemas-upnp-org'
246 urnDomain = strings.Split(d.ServiceType, ":")[1]
247 url = combineURL(rootURL, d.ControlURL)
251 func combineURL(rootURL, subURL string) string {
253 protoEndIndex := strings.Index(rootURL, protocolEnd)
254 a := rootURL[protoEndIndex+len(protocolEnd):]
255 rootIndex := strings.Index(a, "/")
256 return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL
259 func soapRequest(url, function, message, domain string) (r *http.Response, err error) {
260 fullMessage := "<?xml version=\"1.0\" ?>" +
261 "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" +
262 "<s:Body>" + message + "</s:Body></s:Envelope>"
264 req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage))
268 req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"")
269 req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3")
270 //req.Header.Set("Transfer-Encoding", "chunked")
271 req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"")
272 req.Header.Set("Connection", "Close")
273 req.Header.Set("Cache-Control", "no-cache")
274 req.Header.Set("Pragma", "no-cache")
276 // log.Stderr("soapRequest ", req)
278 r, err = http.DefaultClient.Do(req)
286 if r.StatusCode >= 400 {
287 // log.Stderr(function, r.StatusCode)
288 err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function)
295 type statusInfo struct {
296 externalIpAddress string
299 func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) {
301 message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
302 "</u:GetExternalIPAddress>"
304 var response *http.Response
305 response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain)
307 defer response.Body.Close() // nolint: errcheck
313 var envelope Envelope
314 data, err := ioutil.ReadAll(response.Body)
319 reader := bytes.NewReader(data)
320 err = xml.NewDecoder(reader).Decode(&envelope)
325 info = statusInfo{envelope.Soap.ExternalIP.IPAddress}
334 // GetExternalAddress returns an external IP. If GetExternalIPAddress action
335 // fails or IP returned is invalid, GetExternalAddress returns an error.
336 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
337 info, err := n.getExternalIPAddress()
341 addr = net.ParseIP(info.externalIpAddress)
343 err = fmt.Errorf("Failed to parse IP: %v", info.externalIpAddress)
349 func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) {
350 // A single concatenation would break ARM compilation.
351 message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
352 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
353 message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>"
354 message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
355 "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" +
356 "<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
357 message += description +
358 "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
359 "</NewLeaseDuration></u:AddPortMapping>"
361 var response *http.Response
362 response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain)
364 defer response.Body.Close() // nolint: errcheck
370 // TODO: check response to see if the port was forwarded
371 // log.Println(message, response)
373 // body, err := ioutil.ReadAll(response.Body)
374 // fmt.Println(string(body), err)
375 mappedExternalPort = externalPort
380 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {
382 message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
383 "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
384 "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" +
385 "</u:DeletePortMapping>"
387 var response *http.Response
388 response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
390 defer response.Body.Close() // nolint: errcheck
396 // TODO: check response to see if the port was deleted
397 // log.Println(message, response)