Browse Source

lib/upnp: Refactor out methods to util with tests, refactor IGD

Audrius Butkevicius 9 years ago
parent
commit
1d17891286
9 changed files with 497 additions and 300 deletions
  1. 16 132
      lib/config/config.go
  2. 2 1
      lib/config/wrapper.go
  3. 5 12
      lib/connections/connections.go
  4. 0 4
      lib/upnp/debug.go
  5. 91 0
      lib/upnp/igd.go
  6. 95 0
      lib/upnp/igd_service.go
  7. 11 151
      lib/upnp/upnp.go
  8. 119 0
      lib/util/utils.go
  9. 158 0
      lib/util/utils_test.go

+ 16 - 132
lib/config/config.go

@@ -11,15 +11,12 @@ import (
 	"encoding/json"
 	"encoding/xml"
 	"io"
-	"math/rand"
-	"net/url"
 	"os"
-	"reflect"
 	"sort"
-	"strconv"
 	"strings"
 
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/util"
 )
 
 const (
@@ -58,9 +55,9 @@ func New(myID protocol.DeviceID) Configuration {
 	cfg.Version = CurrentVersion
 	cfg.OriginalVersion = CurrentVersion
 
-	setDefaults(&cfg)
-	setDefaults(&cfg.Options)
-	setDefaults(&cfg.GUI)
+	util.SetDefaults(&cfg)
+	util.SetDefaults(&cfg.Options)
+	util.SetDefaults(&cfg.GUI)
 
 	cfg.prepare(myID)
 
@@ -70,9 +67,9 @@ func New(myID protocol.DeviceID) Configuration {
 func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
 	var cfg Configuration
 
-	setDefaults(&cfg)
-	setDefaults(&cfg.Options)
-	setDefaults(&cfg.GUI)
+	util.SetDefaults(&cfg)
+	util.SetDefaults(&cfg.Options)
+	util.SetDefaults(&cfg.GUI)
 
 	err := xml.NewDecoder(r).Decode(&cfg)
 	cfg.OriginalVersion = cfg.Version
@@ -84,9 +81,9 @@ func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
 func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
 	var cfg Configuration
 
-	setDefaults(&cfg)
-	setDefaults(&cfg.Options)
-	setDefaults(&cfg.GUI)
+	util.SetDefaults(&cfg)
+	util.SetDefaults(&cfg.Options)
+	util.SetDefaults(&cfg.GUI)
 
 	err := json.NewDecoder(r).Decode(&cfg)
 	cfg.OriginalVersion = cfg.Version
@@ -143,7 +140,7 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
 }
 
 func (cfg *Configuration) prepare(myID protocol.DeviceID) {
-	fillNilSlices(&cfg.Options)
+	util.FillNilSlices(&cfg.Options)
 
 	// Initialize any empty slices
 	if cfg.Folders == nil {
@@ -171,8 +168,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
 		}
 	}
 
-	cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
-	cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
+	cfg.Options.ListenAddress = util.UniqueStrings(cfg.Options.ListenAddress)
+	cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers)
 
 	if cfg.Version > 0 && cfg.Version < OldestHandledVersion {
 		l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
@@ -234,7 +231,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
 	}
 
 	if cfg.GUI.APIKey == "" {
-		cfg.GUI.APIKey = randomString(32)
+		cfg.GUI.APIKey = util.RandomString(32)
 	}
 }
 
@@ -242,14 +239,14 @@ func convertV11V12(cfg *Configuration) {
 	// Change listen address schema
 	for i, addr := range cfg.Options.ListenAddress {
 		if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
-			cfg.Options.ListenAddress[i] = tcpAddr(addr)
+			cfg.Options.ListenAddress[i] = util.Address("tcp", addr)
 		}
 	}
 
 	for i, device := range cfg.Devices {
 		for j, addr := range device.Addresses {
 			if addr != "dynamic" && addr != "" {
-				cfg.Devices[i].Addresses[j] = tcpAddr(addr)
+				cfg.Devices[i].Addresses[j] = util.Address("tcp", addr)
 			}
 		}
 	}
@@ -297,98 +294,6 @@ func convertV10V11(cfg *Configuration) {
 	cfg.Version = 11
 }
 
-func setDefaults(data interface{}) error {
-	s := reflect.ValueOf(data).Elem()
-	t := s.Type()
-
-	for i := 0; i < s.NumField(); i++ {
-		f := s.Field(i)
-		tag := t.Field(i).Tag
-
-		v := tag.Get("default")
-		if len(v) > 0 {
-			switch f.Interface().(type) {
-			case string:
-				f.SetString(v)
-
-			case int:
-				i, err := strconv.ParseInt(v, 10, 64)
-				if err != nil {
-					return err
-				}
-				f.SetInt(i)
-
-			case float64:
-				i, err := strconv.ParseFloat(v, 64)
-				if err != nil {
-					return err
-				}
-				f.SetFloat(i)
-
-			case bool:
-				f.SetBool(v == "true")
-
-			case []string:
-				// We don't do anything with string slices here. Any default
-				// we set will be appended to by the XML decoder, so we fill
-				// those after decoding.
-
-			default:
-				panic(f.Type())
-			}
-		}
-	}
-	return nil
-}
-
-// fillNilSlices sets default value on slices that are still nil.
-func fillNilSlices(data interface{}) error {
-	s := reflect.ValueOf(data).Elem()
-	t := s.Type()
-
-	for i := 0; i < s.NumField(); i++ {
-		f := s.Field(i)
-		tag := t.Field(i).Tag
-
-		v := tag.Get("default")
-		if len(v) > 0 {
-			switch f.Interface().(type) {
-			case []string:
-				if f.IsNil() {
-					// Treat the default as a comma separated slice
-					vs := strings.Split(v, ",")
-					for i := range vs {
-						vs[i] = strings.TrimSpace(vs[i])
-					}
-
-					rv := reflect.MakeSlice(reflect.TypeOf([]string{}), len(vs), len(vs))
-					for i, v := range vs {
-						rv.Index(i).SetString(v)
-					}
-					f.Set(rv)
-				}
-			}
-		}
-	}
-	return nil
-}
-
-func uniqueStrings(ss []string) []string {
-	var m = make(map[string]bool, len(ss))
-	for _, s := range ss {
-		m[strings.Trim(s, " ")] = true
-	}
-
-	var us = make([]string, 0, len(m))
-	for k := range m {
-		us = append(us, k)
-	}
-
-	sort.Strings(us)
-
-	return us
-}
-
 func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
 	for _, device := range devices {
 		if device.DeviceID.Equals(myID) {
@@ -453,24 +358,3 @@ loop:
 	}
 	return devices[0:count]
 }
-
-// randomCharset contains the characters that can make up a randomString().
-const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
-
-// randomString returns a string of random characters (taken from
-// randomCharset) of the specified length.
-func randomString(l int) string {
-	bs := make([]byte, l)
-	for i := range bs {
-		bs[i] = randomCharset[rand.Intn(len(randomCharset))]
-	}
-	return string(bs)
-}
-
-func tcpAddr(host string) string {
-	u := url.URL{
-		Scheme: "tcp",
-		Host:   host,
-	}
-	return u.String()
-}

+ 2 - 1
lib/config/wrapper.go

@@ -13,6 +13,7 @@ import (
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/sync"
+	"github.com/syncthing/syncthing/lib/util"
 )
 
 // The Committer interface is implemented by objects that need to know about
@@ -321,5 +322,5 @@ func (w *Wrapper) GlobalDiscoveryServers() []string {
 			servers = append(servers, srv)
 		}
 	}
-	return uniqueStrings(servers)
+	return util.UniqueStrings(servers)
 }

+ 5 - 12
lib/connections/connections.go

@@ -24,6 +24,7 @@ import (
 	"github.com/syncthing/syncthing/lib/relay"
 	"github.com/syncthing/syncthing/lib/relay/client"
 	"github.com/syncthing/syncthing/lib/upnp"
+	"github.com/syncthing/syncthing/lib/util"
 
 	"github.com/thejerf/suture"
 )
@@ -504,7 +505,7 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string {
 			l.Infoln("Listen address", addrStr, "is invalid:", err)
 			continue
 		}
-		addr, err := net.ResolveTCPAddr("tcp", addrURL.Host)
+		addr, err := net.ResolveTCPAddr(addrURL.Scheme, addrURL.Host)
 		if err != nil {
 			l.Infoln("Listen address", addrStr, "is invalid:", err)
 			continue
@@ -512,13 +513,13 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string {
 
 		if addr.IP == nil || addr.IP.IsUnspecified() {
 			// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
-			addrs = append(addrs, tcpAddr(addr.String()))
+			addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
 		} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
 			// A public address; include as is.
-			addrs = append(addrs, tcpAddr(addr.String()))
+			addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
 		} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
 			// A private IPv4 address.
-			addrs = append(addrs, tcpAddr(addr.String()))
+			addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
 		}
 	}
 
@@ -567,14 +568,6 @@ func isPublicIPv6(ip net.IP) bool {
 	return ip.IsGlobalUnicast()
 }
 
-func tcpAddr(host string) string {
-	u := url.URL{
-		Scheme: "tcp",
-		Host:   host,
-	}
-	return u.String()
-}
-
 // serviceFunc wraps a function to create a suture.Service without stop
 // functionality.
 type serviceFunc func()

+ 0 - 4
lib/upnp/debug.go

@@ -20,7 +20,3 @@ var (
 func init() {
 	l.SetDebug("upnp", strings.Contains(os.Getenv("STTRACE"), "upnp") || os.Getenv("STTRACE") == "all")
 }
-
-func shouldDebug() bool {
-	return l.ShouldDebug("upnp")
-}

+ 91 - 0
lib/upnp/igd.go

@@ -0,0 +1,91 @@
+// Copyright (C) 2016 The Syncthing Authors.
+//
+// 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/.
+
+// 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)
+
+package upnp
+
+import (
+	"net"
+	"net/url"
+	"strings"
+)
+
+// An IGD is a UPnP InternetGatewayDevice.
+type IGD struct {
+	uuid           string
+	friendlyName   string
+	services       []IGDService
+	url            *url.URL
+	localIPAddress net.IP
+}
+
+func (n *IGD) UUID() string {
+	return n.uuid
+}
+
+func (n *IGD) FriendlyName() string {
+	return n.friendlyName
+}
+
+// FriendlyIdentifier returns a friendly identifier (friendly name + IP
+// address) for the IGD.
+func (n *IGD) FriendlyIdentifier() string {
+	return "'" + n.FriendlyName() + "' (" + strings.Split(n.URL().Host, ":")[0] + ")"
+}
+
+func (n *IGD) URL() *url.URL {
+	return n.url
+}
+
+// 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.
+func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
+	for _, service := range n.services {
+		err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// 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.
+func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
+	for _, service := range n.services {
+		err := service.DeletePortMapping(protocol, externalPort)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// GetExternalIPAddress returns the external IP address of the IGD, or an error
+// if no service providing this feature exists.
+func (n *IGD) GetExternalIPAddress() (ip net.IP, err error) {
+	for _, service := range n.services {
+		ip, err = service.GetExternalIPAddress()
+		if err == nil {
+			break
+		}
+	}
+	return
+}
+
+// GetLocalIPAddress returns the IP address of the local network interface
+// which is facing the IGD.
+func (n *IGD) GetLocalIPAddress() net.IP {
+	return n.localIPAddress
+}

+ 95 - 0
lib/upnp/igd_service.go

@@ -0,0 +1,95 @@
+// Copyright (C) 2016 The Syncthing Authors.
+//
+// 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/.
+
+// 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)
+
+package upnp
+
+import (
+	"encoding/xml"
+	"fmt"
+	"net"
+)
+
+// An IGDService is a specific service provided by an IGD.
+type IGDService struct {
+	ID  string
+	URL string
+	URN string
+}
+
+// AddPortMapping adds a port mapping to the specified IGD service.
+func (s *IGDService) AddPortMapping(localIPAddress net.IP, 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>`
+	body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
+
+	response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
+	if err != nil && timeout > 0 {
+		// Try to repair error code 725 - OnlyPermanentLeasesSupported
+		envelope := &soapErrorResponse{}
+		if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
+			return unmarshalErr
+		}
+		if envelope.ErrorCode == 725 {
+			return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0)
+		}
+	}
+
+	return err
+}
+
+// DeletePortMapping deletes a port mapping from the specified IGD service.
+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>`
+	body := fmt.Sprintf(tpl, s.URN, externalPort, protocol)
+
+	_, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 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
+func (s *IGDService) GetExternalIPAddress() (net.IP, error) {
+	tpl := `<u:GetExternalIPAddress xmlns:u="%s" />`
+
+	body := fmt.Sprintf(tpl, s.URN)
+
+	response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body)
+
+	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
+}

+ 11 - 151
lib/upnp/upnp.go

@@ -29,40 +29,6 @@ import (
 	"github.com/syncthing/syncthing/lib/sync"
 )
 
-// An IGD is a UPnP InternetGatewayDevice.
-type IGD struct {
-	uuid           string
-	friendlyName   string
-	services       []IGDService
-	url            *url.URL
-	localIPAddress string
-}
-
-func (n *IGD) UUID() string {
-	return n.uuid
-}
-
-func (n *IGD) FriendlyName() string {
-	return n.friendlyName
-}
-
-// FriendlyIdentifier returns a friendly identifier (friendly name + IP
-// address) for the IGD.
-func (n *IGD) FriendlyIdentifier() string {
-	return "'" + n.FriendlyName() + "' (" + strings.Split(n.URL().Host, ":")[0] + ")"
-}
-
-func (n *IGD) URL() *url.URL {
-	return n.url
-}
-
-// An IGDService is a specific service provided by an IGD.
-type IGDService struct {
-	ID  string
-	URL string
-	URN string
-}
-
 type Protocol string
 
 const (
@@ -126,22 +92,18 @@ nextResult:
 	for result := range resultChan {
 		for _, existingResult := range results {
 			if existingResult.uuid == result.uuid {
-				if shouldDebug() {
-					l.Debugf("Skipping duplicate result %s with services:", result.uuid)
-					for _, service := range result.services {
-						l.Debugf("* [%s] %s", service.ID, service.URL)
-					}
+				l.Debugf("Skipping duplicate result %s with services:", result.uuid)
+				for _, service := range result.services {
+					l.Debugf("* [%s] %s", service.ID, service.URL)
 				}
 				continue nextResult
 			}
 		}
 
 		results = append(results, result)
-		if shouldDebug() {
-			l.Debugf("UPnP discovery result %s with services:", result.uuid)
-			for _, service := range result.services {
-				l.Debugf("* [%s] %s", service.ID, service.URL)
-			}
+		l.Debugf("UPnP discovery result %s with services:", result.uuid)
+		for _, service := range result.services {
+			l.Debugf("* [%s] %s", service.ID, service.URL)
 		}
 	}
 
@@ -286,19 +248,19 @@ func parseResponse(deviceType string, resp []byte) (IGD, error) {
 	}, nil
 }
 
-func localIP(url *url.URL) (string, error) {
-	conn, err := dialer.Dial("tcp", url.Host)
+func localIP(url *url.URL) (net.IP, error) {
+	conn, err := dialer.DialTimeout("tcp", url.Host, time.Second)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	defer conn.Close()
 
 	localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	return localIPAddress, nil
+	return net.ParseIP(localIPAddress), nil
 }
 
 func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
@@ -460,36 +422,6 @@ func soapRequest(url, service, function, message string) ([]byte, error) {
 	return resp, nil
 }
 
-// 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.
-func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
-	for _, service := range n.services {
-		err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// 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.
-func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
-	for _, service := range n.services {
-		err := service.DeletePortMapping(protocol, externalPort)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
 type soapGetExternalIPAddressResponseEnvelope struct {
 	XMLName xml.Name
 	Body    soapGetExternalIPAddressResponseBody `xml:"Body"`
@@ -508,75 +440,3 @@ type soapErrorResponse struct {
 	ErrorCode        int    `xml:"Body>Fault>detail>UPnPError>errorCode"`
 	ErrorDescription string `xml:"Body>Fault>detail>UPnPError>errorDescription"`
 }
-
-// AddPortMapping adds a port mapping to the specified IGD service.
-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>`
-	body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
-
-	response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
-	if err != nil && timeout > 0 {
-		// Try to repair error code 725 - OnlyPermanentLeasesSupported
-		envelope := &soapErrorResponse{}
-		if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
-			return unmarshalErr
-		}
-		if envelope.ErrorCode == 725 {
-			return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0)
-		}
-	}
-
-	return err
-}
-
-// DeletePortMapping deletes a port mapping from the specified IGD service.
-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>`
-	body := fmt.Sprintf(tpl, s.URN, externalPort, protocol)
-
-	_, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// 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
-func (s *IGDService) GetExternalIPAddress() (net.IP, error) {
-	tpl := `<u:GetExternalIPAddress xmlns:u="%s" />`
-
-	body := fmt.Sprintf(tpl, s.URN)
-
-	response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body)
-
-	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
-}

+ 119 - 0
lib/util/utils.go

@@ -0,0 +1,119 @@
+// Copyright (C) 2016 The Syncthing Authors.
+//
+// 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/.
+
+package util
+
+import (
+	"net/url"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+// SetDefaults sets default values on a struct, based on the default annotation.
+func SetDefaults(data interface{}) error {
+	s := reflect.ValueOf(data).Elem()
+	t := s.Type()
+
+	for i := 0; i < s.NumField(); i++ {
+		f := s.Field(i)
+		tag := t.Field(i).Tag
+
+		v := tag.Get("default")
+		if len(v) > 0 {
+			switch f.Interface().(type) {
+			case string:
+				f.SetString(v)
+
+			case int:
+				i, err := strconv.ParseInt(v, 10, 64)
+				if err != nil {
+					return err
+				}
+				f.SetInt(i)
+
+			case float64:
+				i, err := strconv.ParseFloat(v, 64)
+				if err != nil {
+					return err
+				}
+				f.SetFloat(i)
+
+			case bool:
+				f.SetBool(v == "true")
+
+			case []string:
+				// We don't do anything with string slices here. Any default
+				// we set will be appended to by the XML decoder, so we fill
+				// those after decoding.
+
+			default:
+				panic(f.Type())
+			}
+		}
+	}
+	return nil
+}
+
+// UniqueStrings returns a list on unique strings, trimming and sorting them
+// at the same time.
+func UniqueStrings(ss []string) []string {
+	var m = make(map[string]bool, len(ss))
+	for _, s := range ss {
+		m[strings.Trim(s, " ")] = true
+	}
+
+	var us = make([]string, 0, len(m))
+	for k := range m {
+		us = append(us, k)
+	}
+
+	sort.Strings(us)
+
+	return us
+}
+
+// FillNilSlices sets default value on slices that are still nil.
+func FillNilSlices(data interface{}) error {
+	s := reflect.ValueOf(data).Elem()
+	t := s.Type()
+
+	for i := 0; i < s.NumField(); i++ {
+		f := s.Field(i)
+		tag := t.Field(i).Tag
+
+		v := tag.Get("default")
+		if len(v) > 0 {
+			switch f.Interface().(type) {
+			case []string:
+				if f.IsNil() {
+					// Treat the default as a comma separated slice
+					vs := strings.Split(v, ",")
+					for i := range vs {
+						vs[i] = strings.TrimSpace(vs[i])
+					}
+
+					rv := reflect.MakeSlice(reflect.TypeOf([]string{}), len(vs), len(vs))
+					for i, v := range vs {
+						rv.Index(i).SetString(v)
+					}
+					f.Set(rv)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// Address constructs a URL from the given network and hostname.
+func Address(network, host string) string {
+	u := url.URL{
+		Scheme: network,
+		Host:   host,
+	}
+	return u.String()
+}

+ 158 - 0
lib/util/utils_test.go

@@ -0,0 +1,158 @@
+// Copyright (C) 2016 The Syncthing Authors.
+//
+// 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/.
+
+package util
+
+import "testing"
+
+func TestSetDefaults(t *testing.T) {
+	x := &struct {
+		A string  `default:"string"`
+		B int     `default:"2"`
+		C float64 `default:"2.2"`
+		D bool    `default:"true"`
+	}{}
+
+	if x.A != "" {
+		t.Error("string failed")
+	} else if x.B != 0 {
+		t.Error("int failed")
+	} else if x.C != 0 {
+		t.Errorf("float failed")
+	} else if x.D != false {
+		t.Errorf("bool failed")
+	}
+
+	if err := SetDefaults(x); err != nil {
+		t.Error(err)
+	}
+
+	if x.A != "string" {
+		t.Error("string failed")
+	} else if x.B != 2 {
+		t.Error("int failed")
+	} else if x.C != 2.2 {
+		t.Errorf("float failed")
+	} else if x.D != true {
+		t.Errorf("bool failed")
+	}
+}
+
+func TestUniqueStrings(t *testing.T) {
+	tests := []struct {
+		input    []string
+		expected []string
+	}{
+		{
+			[]string{"a", "b"},
+			[]string{"a", "b"},
+		},
+		{
+			[]string{"a", "a"},
+			[]string{"a"},
+		},
+		{
+			[]string{"a", "a", "a", "a"},
+			[]string{"a"},
+		},
+		{
+			nil,
+			nil,
+		},
+		{
+			[]string{"b", "a"},
+			[]string{"a", "b"},
+		},
+		{
+			[]string{"       a     ", "     a  ", "b        ", "    b"},
+			[]string{"a", "b"},
+		},
+	}
+
+	for _, test := range tests {
+		result := UniqueStrings(test.input)
+		if len(result) != len(test.expected) {
+			t.Errorf("%s != %s", result, test.expected)
+		}
+		for i := range result {
+			if test.expected[i] != result[i] {
+				t.Errorf("%s != %s", result, test.expected)
+			}
+		}
+	}
+}
+
+func TestFillNillSlices(t *testing.T) {
+	// Nil
+	x := &struct {
+		A []string `default:"a,b"`
+	}{}
+
+	if x.A != nil {
+		t.Error("not nil")
+	}
+
+	if err := FillNilSlices(x); err != nil {
+		t.Error(err)
+	}
+
+	if len(x.A) != 2 {
+		t.Error("length")
+	}
+
+	// Already provided
+	y := &struct {
+		A []string `default:"c,d,e"`
+	}{[]string{"a", "b"}}
+
+	if len(y.A) != 2 {
+		t.Error("length")
+	}
+
+	if err := FillNilSlices(y); err != nil {
+		t.Error(err)
+	}
+
+	if len(y.A) != 2 {
+		t.Error("length")
+	}
+
+	// Non-nil but empty
+	z := &struct {
+		A []string `default:"c,d,e"`
+	}{[]string{}}
+
+	if len(z.A) != 0 {
+		t.Error("length")
+	}
+
+	if err := FillNilSlices(z); err != nil {
+		t.Error(err)
+	}
+
+	if len(z.A) != 0 {
+		t.Error("length")
+	}
+}
+
+func TestAddress(t *testing.T) {
+	tests := []struct {
+		network string
+		host    string
+		result  string
+	}{
+		{"tcp", "google.com", "tcp://google.com"},
+		{"foo", "google", "foo://google"},
+		{"123", "456", "123://456"},
+	}
+
+	for _, test := range tests {
+		result := Address(test.network, test.host)
+		if result != test.result {
+			t.Errorf("%s != %s", result, test.result)
+		}
+	}
+}