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

This commit is contained in:
Audrius Butkevicius
2016-03-25 20:22:29 +00:00
committed by Jakob Borg
parent 6a3f3f5577
commit 1d17891286
9 changed files with 497 additions and 300 deletions

View File

@@ -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
lib/upnp/igd.go Normal file
View File

@@ -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
lib/upnp/igd_service.go Normal file
View File

@@ -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
}

View File

@@ -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
}