2014-11-16 20:13:20 +00:00
// Copyright (C) 2014 The Syncthing Authors.
2014-09-29 19:43:32 +00:00
//
2015-03-07 20:36:35 +00:00
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
2014-04-18 11:20:42 +00:00
// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go
// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE)
2014-06-01 20:50:14 +00:00
2014-10-18 02:01:25 +00:00
// Package upnp implements UPnP InternetGatewayDevice discovery, querying, and port mapping.
2014-06-01 20:50:14 +00:00
package upnp
2014-04-18 11:20:42 +00:00
import (
"bufio"
"bytes"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
2014-09-26 04:28:53 +00:00
"regexp"
2015-05-17 15:42:26 +00:00
"runtime"
2014-04-18 11:20:42 +00:00
"strings"
"time"
2015-04-22 22:54:31 +00:00
2015-10-12 18:30:14 +00:00
"github.com/syncthing/syncthing/lib/dialer"
2015-08-06 09:29:25 +00:00
"github.com/syncthing/syncthing/lib/sync"
2014-04-18 11:20:42 +00:00
)
2015-04-28 20:32:10 +00:00
// An IGD is a UPnP InternetGatewayDevice.
2014-04-18 11:20:42 +00:00
type IGD struct {
2014-09-26 04:28:53 +00:00
uuid string
friendlyName string
2014-10-18 03:30:47 +00:00
services [ ] IGDService
2014-09-26 04:28:53 +00:00
url * url . URL
localIPAddress string
}
2014-10-19 17:23:12 +00:00
func ( n * IGD ) UUID ( ) string {
return n . uuid
}
func ( n * IGD ) FriendlyName ( ) string {
return n . friendlyName
}
2015-04-28 20:32:10 +00:00
// FriendlyIdentifier returns a friendly identifier (friendly name + IP
// address) for the IGD.
2014-10-19 17:23:12 +00:00
func ( n * IGD ) FriendlyIdentifier ( ) string {
return "'" + n . FriendlyName ( ) + "' (" + strings . Split ( n . URL ( ) . Host , ":" ) [ 0 ] + ")"
}
func ( n * IGD ) URL ( ) * url . URL {
return n . url
}
2015-04-28 20:32:10 +00:00
// An IGDService is a specific service provided by an IGD.
2014-10-18 03:30:47 +00:00
type IGDService struct {
2015-12-23 15:31:12 +00:00
ID string
URL string
URN string
2014-10-19 17:23:12 +00:00
}
2014-04-18 11:20:42 +00:00
type Protocol string
const (
TCP Protocol = "TCP"
UDP = "UDP"
)
type upnpService struct {
2015-12-23 15:31:12 +00:00
ID string ` xml:"serviceId" `
Type string ` xml:"serviceType" `
ControlURL string ` xml:"controlURL" `
2014-04-18 11:20:42 +00:00
}
type upnpDevice struct {
2014-09-26 04:28:53 +00:00
DeviceType string ` xml:"deviceType" `
FriendlyName string ` xml:"friendlyName" `
Devices [ ] upnpDevice ` xml:"deviceList>device" `
Services [ ] upnpService ` xml:"serviceList>service" `
2014-04-18 11:20:42 +00:00
}
type upnpRoot struct {
Device upnpDevice ` xml:"device" `
}
2014-10-18 02:01:25 +00:00
// Discover discovers UPnP InternetGatewayDevices.
2015-04-16 09:26:09 +00:00
// The order in which the devices appear in the results list is not deterministic.
2015-04-15 23:34:27 +00:00
func Discover ( timeout time . Duration ) [ ] IGD {
2015-04-16 09:26:09 +00:00
var results [ ] IGD
2014-04-18 11:20:42 +00:00
2015-04-16 09:26:09 +00:00
interfaces , err := net . Interfaces ( )
if err != nil {
l . Infoln ( "Listing network interfaces:" , err )
return results
}
2014-09-26 04:28:53 +00:00
2015-04-28 08:29:08 +00:00
resultChan := make ( chan IGD )
2015-04-16 09:26:09 +00:00
2015-04-22 22:54:31 +00:00
wg := sync . NewWaitGroup ( )
2015-05-23 21:27:02 +00:00
2015-04-16 09:26:09 +00:00
for _ , intf := range interfaces {
2015-05-17 15:42:26 +00:00
// Interface flags seem to always be 0 on Windows
if runtime . GOOS != "windows" && ( intf . Flags & net . FlagUp == 0 || intf . Flags & net . FlagMulticast == 0 ) {
2015-04-28 08:29:08 +00:00
continue
}
2015-04-16 09:26:09 +00:00
for _ , deviceType := range [ ] string { "urn:schemas-upnp-org:device:InternetGatewayDevice:1" , "urn:schemas-upnp-org:device:InternetGatewayDevice:2" } {
wg . Add ( 1 )
go func ( intf net . Interface , deviceType string ) {
discover ( & intf , deviceType , timeout , resultChan )
wg . Done ( )
} ( intf , deviceType )
}
2014-04-18 11:20:42 +00:00
}
2015-05-23 21:27:02 +00:00
go func ( ) {
wg . Wait ( )
close ( resultChan )
} ( )
nextResult :
for result := range resultChan {
for _ , existingResult := range results {
if existingResult . uuid == result . uuid {
2015-10-03 15:25:21 +00:00
if shouldDebug ( ) {
2015-05-23 21:27:02 +00:00
l . Debugf ( "Skipping duplicate result %s with services:" , result . uuid )
2015-12-23 15:31:12 +00:00
for _ , service := range result . services {
l . Debugf ( "* [%s] %s" , service . ID , service . URL )
2015-05-23 21:27:02 +00:00
}
}
continue nextResult
}
}
results = append ( results , result )
2015-10-03 15:25:21 +00:00
if shouldDebug ( ) {
2015-05-23 21:27:02 +00:00
l . Debugf ( "UPnP discovery result %s with services:" , result . uuid )
2015-12-23 15:31:12 +00:00
for _ , service := range result . services {
l . Debugf ( "* [%s] %s" , service . ID , service . URL )
2015-05-23 21:27:02 +00:00
}
}
}
2015-04-16 09:26:09 +00:00
return results
2014-09-26 04:28:53 +00:00
}
2014-10-18 02:01:25 +00:00
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
2014-09-26 04:28:53 +00:00
// The order in which the devices appear in the result list is not deterministic
2015-04-16 09:26:09 +00:00
func discover ( intf * net . Interface , deviceType string , timeout time . Duration , results chan <- IGD ) {
2014-09-26 04:28:53 +00:00
ssdp := & net . UDPAddr { IP : [ ] byte { 239 , 255 , 255 , 250 } , Port : 1900 }
tpl := ` M - SEARCH * HTTP / 1.1
2015-10-31 15:36:08 +00:00
HOST : 239.255 .255 .250 : 1900
ST : % s
MAN : "ssdp:discover"
MX : % d
USER - AGENT : syncthing / 1.0
2014-04-18 11:20:42 +00:00
2014-05-28 14:04:20 +00:00
`
2015-04-15 23:34:27 +00:00
searchStr := fmt . Sprintf ( tpl , deviceType , timeout / time . Second )
2014-09-26 04:28:53 +00:00
2014-05-28 14:04:20 +00:00
search := [ ] byte ( strings . Replace ( searchStr , "\n" , "\r\n" , - 1 ) )
2014-04-18 11:20:42 +00:00
2015-10-03 15:25:21 +00:00
l . Debugln ( "Starting discovery of device type" , deviceType , "on" , intf . Name )
2014-09-26 04:28:53 +00:00
2015-04-16 09:26:09 +00:00
socket , err := net . ListenMulticastUDP ( "udp4" , intf , & net . UDPAddr { IP : ssdp . IP } )
2014-04-18 11:20:42 +00:00
if err != nil {
2015-10-03 15:25:21 +00:00
l . Debugln ( err )
2015-04-16 09:26:09 +00:00
return
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
defer socket . Close ( ) // Make sure our socket gets closed
2014-04-18 11:20:42 +00:00
2015-04-15 23:34:27 +00:00
err = socket . SetDeadline ( time . Now ( ) . Add ( timeout ) )
2014-04-18 11:20:42 +00:00
if err != nil {
2014-09-26 04:28:53 +00:00
l . Infoln ( err )
2015-04-16 09:26:09 +00:00
return
2014-04-18 11:20:42 +00:00
}
2015-10-03 15:25:21 +00:00
l . Debugln ( "Sending search request for device type" , deviceType , "on" , intf . Name )
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
_ , err = socket . WriteTo ( search , ssdp )
if err != nil {
l . Infoln ( err )
2015-04-16 09:26:09 +00:00
return
2014-09-26 04:28:53 +00:00
}
2015-10-03 15:25:21 +00:00
l . Debugln ( "Listening for UPnP response for device type" , deviceType , "on" , intf . Name )
2014-09-26 04:28:53 +00:00
// Listen for responses until a timeout is reached
for {
2015-04-28 08:29:08 +00:00
resp := make ( [ ] byte , 65536 )
2014-09-26 04:28:53 +00:00
n , _ , err := socket . ReadFrom ( resp )
if err != nil {
if e , ok := err . ( net . Error ) ; ! ok || ! e . Timeout ( ) {
2015-04-16 14:40:01 +00:00
l . Infoln ( "UPnP read:" , err ) //legitimate error, not a timeout.
2014-09-26 04:28:53 +00:00
}
break
}
2015-04-16 09:26:09 +00:00
igd , err := parseResponse ( deviceType , resp [ : n ] )
if err != nil {
2015-04-16 14:40:01 +00:00
l . Infoln ( "UPnP parse:" , err )
2015-04-16 09:26:09 +00:00
continue
2014-10-23 02:09:17 +00:00
}
2015-04-16 09:26:09 +00:00
results <- igd
2014-09-26 04:28:53 +00:00
}
2015-10-03 15:25:21 +00:00
l . Debugln ( "Discovery for device type" , deviceType , "on" , intf . Name , "finished." )
2014-09-26 04:28:53 +00:00
}
2015-04-16 09:26:09 +00:00
func parseResponse ( deviceType string , resp [ ] byte ) ( IGD , error ) {
2015-10-03 15:25:21 +00:00
l . Debugln ( "Handling UPnP response:\n\n" + string ( resp ) )
2014-09-26 04:28:53 +00:00
2015-04-16 09:26:09 +00:00
reader := bufio . NewReader ( bytes . NewBuffer ( resp ) )
2014-04-18 11:20:42 +00:00
request := & http . Request { }
response , err := http . ReadResponse ( reader , request )
2014-05-16 17:04:32 +00:00
if err != nil {
2015-04-16 09:26:09 +00:00
return IGD { } , err
2014-05-16 17:04:32 +00:00
}
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
respondingDeviceType := response . Header . Get ( "St" )
if respondingDeviceType != deviceType {
2015-04-16 09:26:09 +00:00
return IGD { } , errors . New ( "unrecognized UPnP device of type " + respondingDeviceType )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceDescriptionLocation := response . Header . Get ( "Location" )
if deviceDescriptionLocation == "" {
2015-04-28 20:32:10 +00:00
return IGD { } , errors . New ( "invalid IGD response: no location specified" )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceDescriptionURL , err := url . Parse ( deviceDescriptionLocation )
2014-04-18 11:20:42 +00:00
if err != nil {
2014-09-26 04:28:53 +00:00
l . Infoln ( "Invalid IGD location: " + err . Error ( ) )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceUSN := response . Header . Get ( "USN" )
if deviceUSN == "" {
2015-04-28 20:32:10 +00:00
return IGD { } , errors . New ( "invalid IGD response: USN not specified" )
2014-09-26 04:28:53 +00:00
}
2015-05-05 11:53:11 +00:00
deviceUUID := strings . TrimPrefix ( strings . Split ( deviceUSN , "::" ) [ 0 ] , "uuid:" )
2014-09-26 04:28:53 +00:00
matched , err := regexp . MatchString ( "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" , deviceUUID )
if ! matched {
2014-10-21 11:12:57 +00:00
l . Infoln ( "Invalid IGD response: invalid device UUID" , deviceUUID , "(continuing anyway)" )
2014-09-26 04:28:53 +00:00
}
response , err = http . Get ( deviceDescriptionLocation )
if err != nil {
2015-04-16 09:26:09 +00:00
return IGD { } , err
2014-09-26 04:28:53 +00:00
}
defer response . Body . Close ( )
if response . StatusCode >= 400 {
2015-04-16 09:26:09 +00:00
return IGD { } , errors . New ( "bad status code:" + response . Status )
2014-09-26 04:28:53 +00:00
}
var upnpRoot upnpRoot
err = xml . NewDecoder ( response . Body ) . Decode ( & upnpRoot )
if err != nil {
2015-04-16 09:26:09 +00:00
return IGD { } , err
2014-09-26 04:28:53 +00:00
}
services , err := getServiceDescriptions ( deviceDescriptionLocation , upnpRoot . Device )
if err != nil {
2015-04-16 09:26:09 +00:00
return IGD { } , err
2014-09-26 04:28:53 +00:00
}
// Figure out our IP number, on the network used to reach the IGD.
// We do this in a fairly roundabout way by connecting to the IGD and
2014-04-18 11:20:42 +00:00
// checking the address of the local end of the socket. I'm open to
// suggestions on a better way to do this...
2014-09-26 04:28:53 +00:00
localIPAddress , err := localIP ( deviceDescriptionURL )
2014-04-18 11:20:42 +00:00
if err != nil {
2015-04-16 09:26:09 +00:00
return IGD { } , err
2014-04-18 11:20:42 +00:00
}
2015-04-16 09:26:09 +00:00
return IGD {
2014-09-26 04:28:53 +00:00
uuid : deviceUUID ,
friendlyName : upnpRoot . Device . FriendlyName ,
url : deviceDescriptionURL ,
services : services ,
localIPAddress : localIPAddress ,
2015-04-16 09:26:09 +00:00
} , nil
2014-09-26 04:28:53 +00:00
}
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
func localIP ( url * url . URL ) ( string , error ) {
2015-10-12 18:30:14 +00:00
conn , err := dialer . Dial ( "tcp" , url . Host )
2014-04-18 11:20:42 +00:00
if err != nil {
return "" , err
}
defer conn . Close ( )
2014-09-26 04:28:53 +00:00
localIPAddress , _ , err := net . SplitHostPort ( conn . LocalAddr ( ) . String ( ) )
2014-04-18 11:20:42 +00:00
if err != nil {
return "" , err
}
2014-09-26 04:28:53 +00:00
return localIPAddress , nil
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
func getChildDevices ( d upnpDevice , deviceType string ) [ ] upnpDevice {
2014-12-08 16:07:55 +00:00
var result [ ] upnpDevice
2014-04-18 11:20:42 +00:00
for _ , dev := range d . Devices {
if dev . DeviceType == deviceType {
2014-09-26 04:28:53 +00:00
result = append ( result , dev )
2014-04-18 11:20:42 +00:00
}
}
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
func getChildServices ( d upnpDevice , serviceType string ) [ ] upnpService {
2014-12-08 16:07:55 +00:00
var result [ ] upnpService
2015-12-23 15:31:12 +00:00
for _ , service := range d . Services {
if service . Type == serviceType {
result = append ( result , service )
2014-04-18 11:20:42 +00:00
}
}
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2014-10-18 03:30:47 +00:00
func getServiceDescriptions ( rootURL string , device upnpDevice ) ( [ ] IGDService , error ) {
2014-12-08 16:07:55 +00:00
var result [ ] IGDService
2014-09-26 04:28:53 +00:00
if device . DeviceType == "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
2014-10-18 03:30:47 +00:00
descriptions := getIGDServices ( rootURL , device ,
2014-09-26 04:28:53 +00:00
"urn:schemas-upnp-org:device:WANDevice:1" ,
"urn:schemas-upnp-org:device:WANConnectionDevice:1" ,
[ ] string { "urn:schemas-upnp-org:service:WANIPConnection:1" , "urn:schemas-upnp-org:service:WANPPPConnection:1" } )
result = append ( result , descriptions ... )
} else if device . DeviceType == "urn:schemas-upnp-org:device:InternetGatewayDevice:2" {
2014-10-18 03:30:47 +00:00
descriptions := getIGDServices ( rootURL , device ,
2014-09-26 04:28:53 +00:00
"urn:schemas-upnp-org:device:WANDevice:2" ,
"urn:schemas-upnp-org:device:WANConnectionDevice:2" ,
2015-08-09 12:14:13 +00:00
[ ] string { "urn:schemas-upnp-org:service:WANIPConnection:2" , "urn:schemas-upnp-org:service:WANPPPConnection:2" } )
2014-09-26 04:28:53 +00:00
result = append ( result , descriptions ... )
} else {
return result , errors . New ( "[" + rootURL + "] Malformed root device description: not an InternetGatewayDevice." )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
if len ( result ) < 1 {
return result , errors . New ( "[" + rootURL + "] Malformed device description: no compatible service descriptions found." )
2014-04-18 11:20:42 +00:00
}
2015-04-28 20:32:10 +00:00
return result , nil
2014-07-22 17:23:43 +00:00
}
2014-04-18 11:20:42 +00:00
2015-12-23 15:31:12 +00:00
func getIGDServices ( rootURL string , device upnpDevice , wanDeviceURN string , wanConnectionURN string , URNs [ ] string ) [ ] IGDService {
2014-12-08 16:07:55 +00:00
var result [ ] IGDService
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
devices := getChildDevices ( device , wanDeviceURN )
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
if len ( devices ) < 1 {
2015-10-03 15:25:21 +00:00
l . Infoln ( rootURL , "- malformed InternetGatewayDevice description: no WANDevices specified." )
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
for _ , device := range devices {
connections := getChildDevices ( device , wanConnectionURN )
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
if len ( connections ) < 1 {
2015-10-03 15:25:21 +00:00
l . Infoln ( rootURL , "- malformed " , wanDeviceURN , "description: no WANConnectionDevices specified." )
2014-09-26 04:28:53 +00:00
}
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
for _ , connection := range connections {
2015-12-23 15:31:12 +00:00
for _ , URN := range URNs {
services := getChildServices ( connection , URN )
2014-09-26 04:28:53 +00:00
2015-12-23 15:31:12 +00:00
l . Debugln ( rootURL , "- no services of type" , URN , " found on connection." )
2014-09-26 04:28:53 +00:00
for _ , service := range services {
if len ( service . ControlURL ) == 0 {
2015-12-23 15:31:12 +00:00
l . Infoln ( rootURL + "- malformed" , service . Type , "description: no control URL." )
2014-09-26 04:28:53 +00:00
} else {
u , _ := url . Parse ( rootURL )
replaceRawPath ( u , service . ControlURL )
2015-12-23 15:31:12 +00:00
l . Debugln ( rootURL , "- found" , service . Type , "with URL" , u )
2014-09-26 04:28:53 +00:00
2015-12-23 15:31:12 +00:00
service := IGDService { ID : service . ID , URL : u . String ( ) , URN : service . Type }
2014-10-18 03:30:47 +00:00
result = append ( result , service )
2014-09-26 04:28:53 +00:00
}
}
}
}
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
return result
2014-07-02 18:28:03 +00:00
}
func replaceRawPath ( u * url . URL , rp string ) {
2015-01-08 05:23:20 +00:00
asURL , err := url . Parse ( rp )
if err != nil {
return
} else if asURL . IsAbs ( ) {
u . Path = asURL . Path
u . RawQuery = asURL . RawQuery
2014-04-18 11:20:42 +00:00
} else {
2015-01-08 05:23:20 +00:00
var p , q string
fs := strings . Split ( rp , "?" )
p = fs [ 0 ]
if len ( fs ) > 1 {
q = fs [ 1 ]
}
if p [ 0 ] == '/' {
u . Path = p
} else {
u . Path += p
}
u . RawQuery = q
2014-04-18 11:20:42 +00:00
}
}
2014-10-19 17:38:12 +00:00
func soapRequest ( url , service , function , message string ) ( [ ] byte , error ) {
2014-10-27 23:33:26 +00:00
tpl := ` < ? xml version = "1.0" ? >
2014-04-18 11:20:42 +00:00
< s : Envelope xmlns : s = "http://schemas.xmlsoap.org/soap/envelope/" s : encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" >
< s : Body > % s < / s : Body >
< / s : Envelope >
`
2014-10-18 03:30:47 +00:00
var resp [ ] byte
2014-04-18 11:20:42 +00:00
body := fmt . Sprintf ( tpl , message )
req , err := http . NewRequest ( "POST" , url , strings . NewReader ( body ) )
if err != nil {
2014-10-18 03:30:47 +00:00
return resp , err
2014-04-18 11:20:42 +00:00
}
2015-04-26 20:31:43 +00:00
req . Close = true
2014-04-18 11:20:42 +00:00
req . Header . Set ( "Content-Type" , ` text/xml; charset="utf-8" ` )
req . Header . Set ( "User-Agent" , "syncthing/1.0" )
2015-04-26 10:37:37 +00:00
req . Header [ "SOAPAction" ] = [ ] string { fmt . Sprintf ( ` "%s#%s" ` , service , function ) } // Enforce capitalization in header-entry for sensitive routers. See issue #1696
2014-04-18 11:20:42 +00:00
req . Header . Set ( "Connection" , "Close" )
req . Header . Set ( "Cache-Control" , "no-cache" )
req . Header . Set ( "Pragma" , "no-cache" )
2015-10-03 15:25:21 +00:00
l . Debugln ( "SOAP Request URL: " + url )
l . Debugln ( "SOAP Action: " + req . Header . Get ( "SOAPAction" ) )
l . Debugln ( "SOAP Request:\n\n" + body )
2014-04-18 11:20:42 +00:00
r , err := http . DefaultClient . Do ( req )
if err != nil {
2015-10-03 15:25:21 +00:00
l . Debugln ( err )
2014-10-18 03:30:47 +00:00
return resp , err
2014-04-18 11:20:42 +00:00
}
2014-10-18 03:30:47 +00:00
resp , _ = ioutil . ReadAll ( r . Body )
2015-10-03 15:25:21 +00:00
l . Debugf ( "SOAP Response: %s\n\n%s\n\n" , r . Status , resp )
2014-04-18 11:20:42 +00:00
r . Body . Close ( )
if r . StatusCode >= 400 {
2014-10-18 03:30:47 +00:00
return resp , errors . New ( function + ": " + r . Status )
2014-04-18 11:20:42 +00:00
}
2014-10-18 03:30:47 +00:00
return resp , nil
2014-04-18 11:20:42 +00:00
}
2015-04-28 20:32:10 +00:00
// AddPortMapping adds a port mapping to all relevant services on the
// specified InternetGatewayDevice. Port mapping will fail and return an error
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
2014-04-18 11:20:42 +00:00
func ( n * IGD ) AddPortMapping ( protocol Protocol , externalPort , internalPort int , description string , timeout int ) error {
2014-09-26 04:28:53 +00:00
for _ , service := range n . services {
2014-10-18 17:20:57 +00:00
err := service . AddPortMapping ( n . localIPAddress , protocol , externalPort , internalPort , description , timeout )
2014-09-26 04:28:53 +00:00
if err != nil {
return err
}
}
return nil
2014-04-18 11:20:42 +00:00
}
2015-04-28 20:32:10 +00:00
// DeletePortMapping deletes a port mapping from all relevant services on the
// specified InternetGatewayDevice. Port mapping will fail and return an error
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
2014-10-18 17:20:57 +00:00
func ( n * IGD ) DeletePortMapping ( protocol Protocol , externalPort int ) error {
2014-09-26 04:28:53 +00:00
for _ , service := range n . services {
2014-10-18 17:20:57 +00:00
err := service . DeletePortMapping ( protocol , externalPort )
2014-09-26 04:28:53 +00:00
if err != nil {
return err
}
}
return nil
}
2014-10-18 03:30:47 +00:00
type soapGetExternalIPAddressResponseEnvelope struct {
XMLName xml . Name
Body soapGetExternalIPAddressResponseBody ` xml:"Body" `
}
type soapGetExternalIPAddressResponseBody struct {
XMLName xml . Name
GetExternalIPAddressResponse getExternalIPAddressResponse ` xml:"GetExternalIPAddressResponse" `
}
type getExternalIPAddressResponse struct {
NewExternalIPAddress string ` xml:"NewExternalIPAddress" `
}
2015-05-21 07:55:55 +00:00
type soapErrorResponse struct {
ErrorCode int ` xml:"Body>Fault>detail>UPnPError>errorCode" `
ErrorDescription string ` xml:"Body>Fault>detail>UPnPError>errorDescription" `
}
2015-04-28 20:32:10 +00:00
// AddPortMapping adds a port mapping to the specified IGD service.
2014-10-18 17:20:57 +00:00
func ( s * IGDService ) AddPortMapping ( localIPAddress string , protocol Protocol , externalPort , internalPort int , description string , timeout int ) error {
tpl := ` < u : AddPortMapping xmlns : u = "%s" >
< NewRemoteHost > < / NewRemoteHost >
< NewExternalPort > % d < / NewExternalPort >
< NewProtocol > % s < / NewProtocol >
< NewInternalPort > % d < / NewInternalPort >
< NewInternalClient > % s < / NewInternalClient >
< NewEnabled > 1 < / NewEnabled >
< NewPortMappingDescription > % s < / NewPortMappingDescription >
< NewLeaseDuration > % d < / NewLeaseDuration >
< / u : AddPortMapping > `
2015-12-23 15:31:12 +00:00
body := fmt . Sprintf ( tpl , s . URN , externalPort , protocol , internalPort , localIPAddress , description , timeout )
2014-10-18 17:20:57 +00:00
2015-12-23 15:31:12 +00:00
response , err := soapRequest ( s . URL , s . URN , "AddPortMapping" , body )
2015-05-21 07:55:55 +00:00
if err != nil && timeout > 0 {
// Try to repair error code 725 - OnlyPermanentLeasesSupported
envelope := & soapErrorResponse { }
2015-12-13 09:38:21 +00:00
if unmarshalErr := xml . Unmarshal ( response , envelope ) ; unmarshalErr != nil {
return unmarshalErr
2015-05-21 07:55:55 +00:00
}
if envelope . ErrorCode == 725 {
return s . AddPortMapping ( localIPAddress , protocol , externalPort , internalPort , description , 0 )
}
2014-10-18 17:20:57 +00:00
}
2015-05-21 07:55:55 +00:00
return err
2014-10-18 17:20:57 +00:00
}
2015-04-28 20:32:10 +00:00
// DeletePortMapping deletes a port mapping from the specified IGD service.
2014-10-18 17:20:57 +00:00
func ( s * IGDService ) DeletePortMapping ( protocol Protocol , externalPort int ) error {
tpl := ` < u : DeletePortMapping xmlns : u = "%s" >
< NewRemoteHost > < / NewRemoteHost >
< NewExternalPort > % d < / NewExternalPort >
< NewProtocol > % s < / NewProtocol >
< / u : DeletePortMapping > `
2015-12-23 15:31:12 +00:00
body := fmt . Sprintf ( tpl , s . URN , externalPort , protocol )
2014-10-18 17:20:57 +00:00
2015-12-23 15:31:12 +00:00
_ , err := soapRequest ( s . URL , s . URN , "DeletePortMapping" , body )
2014-10-18 17:20:57 +00:00
if err != nil {
return err
}
return nil
}
2015-04-28 20:32:10 +00:00
// GetExternalIPAddress queries the IGD service for its external IP address.
// Returns nil if the external IP address is invalid or undefined, along with
// any relevant errors
2014-10-18 03:30:47 +00:00
func ( s * IGDService ) GetExternalIPAddress ( ) ( net . IP , error ) {
tpl := ` <u:GetExternalIPAddress xmlns:u="%s" /> `
2015-12-23 15:31:12 +00:00
body := fmt . Sprintf ( tpl , s . URN )
2014-10-18 03:30:47 +00:00
2015-12-23 15:31:12 +00:00
response , err := soapRequest ( s . URL , s . URN , "GetExternalIPAddress" , body )
2014-10-18 03:30:47 +00:00
if err != nil {
return nil , err
}
envelope := & soapGetExternalIPAddressResponseEnvelope { }
err = xml . Unmarshal ( response , envelope )
if err != nil {
return nil , err
}
result := net . ParseIP ( envelope . Body . GetExternalIPAddressResponse . NewExternalIPAddress )
return result , nil
}