Compare commits
4 Commits
will/statu
...
jknodt/upn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a61e1b32 | ||
|
|
cb2d9c13fe | ||
|
|
c6b92ddda8 | ||
|
|
caceeff374 |
@@ -40,6 +40,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tempfork/upnp from tailscale.com/tempfork/upnp/dcps/internetgateway2
|
||||
tailscale.com/tempfork/upnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
tailscale.com/tempfork/upnp/httpu from tailscale.com/tempfork/upnp
|
||||
tailscale.com/tempfork/upnp/scpd from tailscale.com/tempfork/upnp
|
||||
tailscale.com/tempfork/upnp/soap from tailscale.com/tempfork/upnp+
|
||||
tailscale.com/tempfork/upnp/ssdp from tailscale.com/tempfork/upnp
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
@@ -76,7 +82,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
@@ -126,7 +132,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
|
||||
@@ -118,6 +118,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
tailscale.com/tempfork/upnp from tailscale.com/tempfork/upnp/dcps/internetgateway2
|
||||
tailscale.com/tempfork/upnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
tailscale.com/tempfork/upnp/httpu from tailscale.com/tempfork/upnp
|
||||
tailscale.com/tempfork/upnp/scpd from tailscale.com/tempfork/upnp
|
||||
tailscale.com/tempfork/upnp/soap from tailscale.com/tempfork/upnp+
|
||||
tailscale.com/tempfork/upnp/ssdp from tailscale.com/tempfork/upnp
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
@@ -178,7 +184,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp+
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/mdlayher/netlink+
|
||||
@@ -232,6 +238,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from tailscale.com/tempfork/upnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
|
||||
2
go.mod
2
go.mod
@@ -32,7 +32,7 @@ require (
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
|
||||
4
go.sum
4
go.sum
@@ -685,6 +685,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -716,8 +717,9 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
||||
@@ -12,4 +12,4 @@ func networkIsUnreachable(err error) bool { return false }
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
@@ -57,18 +56,31 @@ type Client struct {
|
||||
pmpPubIPTime time.Time // time pmpPubIP last verified
|
||||
pmpLastEpoch uint32
|
||||
|
||||
pcpSawTime time.Time // time we last saw PCP was available
|
||||
uPnPSawTime time.Time // time we last saw UPnP was available
|
||||
localPort uint16
|
||||
|
||||
localPort uint16
|
||||
pmpMapping *pmpMapping // non-nil if we have a PMP mapping
|
||||
mapping // non-nil if we have a mapping
|
||||
|
||||
// Prober is this portmappers stateful mechanism for detecting when portmapping services are
|
||||
// available on the current network. It is exposed so that clients can pause or stop probing.
|
||||
// In order to create a prober, either call `Probe()` or `NewProber()`, which will populate
|
||||
// this field.
|
||||
*Prober
|
||||
}
|
||||
|
||||
// Mapping represents a created port-mapping over some protocol. It specifies a lease duration,
|
||||
// how to release the mapping, and whether the map is still valid.
|
||||
type mapping interface {
|
||||
isCurrent() bool
|
||||
release()
|
||||
validUntil() time.Time
|
||||
externalIPPort() netaddr.IPPort
|
||||
}
|
||||
|
||||
// HaveMapping reports whether we have a current valid mapping.
|
||||
func (c *Client) HaveMapping() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.pmpMapping != nil && c.pmpMapping.useUntil.After(time.Now())
|
||||
return c.mapping != nil && c.mapping.isCurrent()
|
||||
}
|
||||
|
||||
// pmpMapping is an already-created PMP mapping.
|
||||
@@ -87,6 +99,10 @@ func (m *pmpMapping) externalValid() bool {
|
||||
return !m.external.IP().IsZero() && m.external.Port() != 0
|
||||
}
|
||||
|
||||
func (p *pmpMapping) isCurrent() bool { return p.useUntil.After(time.Now()) }
|
||||
func (p *pmpMapping) validUntil() time.Time { return p.useUntil }
|
||||
func (p *pmpMapping) externalIPPort() netaddr.IPPort { return p.external }
|
||||
|
||||
// release does a best effort fire-and-forget release of the PMP mapping m.
|
||||
func (m *pmpMapping) release() {
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
@@ -119,8 +135,8 @@ func (c *Client) SetGatewayLookupFunc(f func() (gw, myIP netaddr.IP, ok bool)) {
|
||||
// comes back.
|
||||
func (c *Client) NoteNetworkDown() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.invalidateMappingsLocked(false)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
@@ -154,7 +170,6 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
gw = netaddr.IP{}
|
||||
myIP = netaddr.IP{}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -167,16 +182,14 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
}
|
||||
|
||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||
if c.pmpMapping != nil {
|
||||
if c.mapping != nil {
|
||||
if releaseOld {
|
||||
c.pmpMapping.release()
|
||||
c.mapping.release()
|
||||
}
|
||||
c.pmpMapping = nil
|
||||
c.mapping = nil
|
||||
}
|
||||
c.pmpPubIP = netaddr.IP{}
|
||||
c.pmpPubIPTime = time.Time{}
|
||||
c.pcpSawTime = time.Time{}
|
||||
c.uPnPSawTime = time.Time{}
|
||||
}
|
||||
|
||||
func (c *Client) sawPMPRecently() bool {
|
||||
@@ -190,15 +203,19 @@ func (c *Client) sawPMPRecentlyLocked() bool {
|
||||
}
|
||||
|
||||
func (c *Client) sawPCPRecently() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
if c.Prober == nil {
|
||||
return false
|
||||
}
|
||||
present, _ := c.Prober.PCP.PresentCurrent()
|
||||
return present
|
||||
}
|
||||
|
||||
func (c *Client) sawUPnPRecently() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
if c.Prober == nil {
|
||||
return false
|
||||
}
|
||||
present, _ := c.Prober.UPnP.PresentCurrent()
|
||||
return present
|
||||
}
|
||||
|
||||
// closeCloserOnContextDone starts a new goroutine to call c.Close
|
||||
@@ -241,6 +258,11 @@ var (
|
||||
ErrGatewayNotFound = errors.New("failed to look up gateway address")
|
||||
)
|
||||
|
||||
// Probe starts a periodic probe and blocks until the first result of probing.
|
||||
func (c *Client) Probe(ctx context.Context) (ProbeResult, error) {
|
||||
return c.NewProber(ctx).StatusBlock()
|
||||
}
|
||||
|
||||
// CreateOrGetMapping either creates a new mapping or returns a cached
|
||||
// valid one.
|
||||
//
|
||||
@@ -254,9 +276,10 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
|
||||
c.mu.Lock()
|
||||
localPort := c.localPort
|
||||
internalAddr := netaddr.IPPortFrom(myIP, localPort)
|
||||
m := &pmpMapping{
|
||||
gw: gw,
|
||||
internal: netaddr.IPPortFrom(myIP, localPort),
|
||||
internal: internalAddr,
|
||||
}
|
||||
|
||||
// prevPort is the port we had most previously, if any. We try
|
||||
@@ -265,13 +288,13 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
|
||||
// Do we have an existing mapping that's valid?
|
||||
now := time.Now()
|
||||
if m := c.pmpMapping; m != nil {
|
||||
if now.Before(m.useUntil) {
|
||||
if m := c.mapping; m != nil {
|
||||
if now.Before(m.validUntil()) {
|
||||
defer c.mu.Unlock()
|
||||
return m.external, nil
|
||||
return m.externalIPPort(), nil
|
||||
}
|
||||
// The mapping might still be valid, so just try to renew it.
|
||||
prevPort = m.external.Port()
|
||||
prevPort = m.externalIPPort().Port()
|
||||
}
|
||||
|
||||
// If we just did a Probe (e.g. via netchecker) but didn't
|
||||
@@ -281,11 +304,11 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if haveRecentPMP {
|
||||
m.external = m.external.WithIP(c.pmpPubIP)
|
||||
}
|
||||
|
||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
||||
c.mu.Unlock()
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
@@ -320,7 +343,8 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if ctx.Err() == context.Canceled {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
// switch to trying UPnP
|
||||
break
|
||||
}
|
||||
srcu := srci.(*net.UDPAddr)
|
||||
src, ok := netaddr.FromStdAddr(srcu.IP, srcu.Port, srcu.Zone)
|
||||
@@ -351,10 +375,12 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if m.externalValid() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.pmpMapping = m
|
||||
c.mapping = m
|
||||
return m.external, nil
|
||||
}
|
||||
}
|
||||
|
||||
return c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort)
|
||||
}
|
||||
|
||||
type pmpResultCode uint16
|
||||
@@ -439,121 +465,6 @@ type ProbeResult struct {
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
// Probe returns a summary of which port mapping services are
|
||||
// available on the network.
|
||||
//
|
||||
// If a probe has run recently and there haven't been any network changes since,
|
||||
// the returned result might be server from the Client's cache, without
|
||||
// sending any network traffic.
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return res, ErrGatewayNotFound
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastProbe = time.Now()
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
}
|
||||
defer uc.Close()
|
||||
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
} else {
|
||||
uc.WriteTo(uPnPPacket, upnpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && res.PMP && res.UPnP {
|
||||
// Nothing more to discover.
|
||||
return res, nil
|
||||
}
|
||||
n, addr, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
port := addr.(*net.UDPAddr).Port
|
||||
switch port {
|
||||
case upnpPort:
|
||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case pcpPort: // same as pmpPort
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
res.PCP = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
res.PCP = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
res.PMP = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
pcpVersion = 2
|
||||
pcpPort = 5351
|
||||
@@ -627,14 +538,4 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
||||
return res, true
|
||||
}
|
||||
|
||||
const (
|
||||
upnpPort = 1900
|
||||
)
|
||||
|
||||
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|
||||
|
||||
@@ -32,12 +32,13 @@ func TestClientProbe(t *testing.T) {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(t.Logf)
|
||||
for i := 0; i < 2; i++ {
|
||||
c.NewProber(context.Background())
|
||||
for i := 0; i < 30; i++ {
|
||||
if i > 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
res, err := c.Probe(context.Background())
|
||||
t.Logf("Got: %+v, %v", res, err)
|
||||
res, err := c.Prober.CurrentStatus()
|
||||
t.Logf("Got(t=%dms): %+v, %v", i*100, res, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
337
net/portmapper/probe.go
Normal file
337
net/portmapper/probe.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
// Prober periodically pings the network and checks for port-mapping services.
|
||||
type Prober struct {
|
||||
// pause signals the probe to either pause temporarily (true), or stop entirely (false)
|
||||
// to restart the probe, send another pause to it.
|
||||
pause chan<- bool
|
||||
|
||||
// Each of the SubResults below is intended to expose whether a specific service is available
|
||||
// for use on a client, and the most recent seen time. Should not be modified externally, and
|
||||
// will be periodically updated.
|
||||
|
||||
// PMP stores the result of probing pmp services and is populated by prober.
|
||||
PMP ProbeSubResult
|
||||
// PCP stores the result of probing pcp services and is populated by prober.
|
||||
PCP ProbeSubResult
|
||||
|
||||
// upnpClient is a reused upnpClient for probing upnp results.
|
||||
upnpClient upnpClient
|
||||
// PCP stores the result of probing pcp services and is populated by prober.
|
||||
UPnP ProbeSubResult
|
||||
}
|
||||
|
||||
// NewProber creates a new prober for a given client.
|
||||
func (c *Client) NewProber(ctx context.Context) *Prober {
|
||||
if c.Prober != nil {
|
||||
return c.Prober
|
||||
}
|
||||
pause := make(chan bool)
|
||||
p := &Prober{
|
||||
pause: pause,
|
||||
|
||||
PMP: NewProbeSubResult(),
|
||||
PCP: NewProbeSubResult(),
|
||||
UPnP: NewProbeSubResult(),
|
||||
}
|
||||
c.Prober = p
|
||||
|
||||
go func() {
|
||||
for {
|
||||
pmpCtx, cancel := context.WithTimeout(ctx, portMapServiceTimeout)
|
||||
hasPCP, hasPMP, err := c.probePMPAndPCP(pmpCtx)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
// the global context has passed, exit cleanly
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if pmpCtx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
p.PMP.Set(hasPMP, err)
|
||||
p.PCP.Set(hasPCP, err)
|
||||
|
||||
t := time.NewTimer(trustServiceStillAvailableDuration * 3 / 4)
|
||||
|
||||
select {
|
||||
case should_pause := <-pause:
|
||||
if !should_pause {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
restart := <-pause
|
||||
if !restart {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
case <-t.C: // break through and retry the connection
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// Do not timeout on getting an initial client, as we can reuse it so paying an initial cost
|
||||
// is fine.
|
||||
upnpClient, err := getUPnPClient(ctx)
|
||||
if upnpClient == nil || err != nil {
|
||||
p.UPnP.Set(false, err)
|
||||
return
|
||||
}
|
||||
p.upnpClient = upnpClient
|
||||
defer func() {
|
||||
// unset client when no longer using it.
|
||||
p.upnpClient = nil
|
||||
upnpClient.RequestTermination(context.Background())
|
||||
}()
|
||||
// TODO maybe do something fancy/dynamic with more delay (exponential back-off)
|
||||
for {
|
||||
upnpCtx, cancel := context.WithTimeout(ctx, portMapServiceTimeout*5)
|
||||
retries := 0
|
||||
hasUPnP := false
|
||||
const num_connect_retries = 5
|
||||
for retries < num_connect_retries {
|
||||
status, _, _, statusErr := p.upnpClient.GetStatusInfo(upnpCtx)
|
||||
if statusErr != nil {
|
||||
err = statusErr
|
||||
break
|
||||
}
|
||||
hasUPnP = hasUPnP || status == "Connected"
|
||||
if status == "Disconnected" {
|
||||
upnpClient.RequestConnection(upnpCtx)
|
||||
}
|
||||
retries += 1
|
||||
}
|
||||
// need to manually check these since GetStatusInfo doesn't take a context
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
// the global context has passed, exit cleanly
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if upnpCtx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
cancel()
|
||||
p.UPnP.Set(hasUPnP, err)
|
||||
|
||||
t := time.NewTimer(trustServiceStillAvailableDuration * 3 / 4)
|
||||
|
||||
select {
|
||||
case should_pause := <-pause:
|
||||
if !should_pause {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
restart := <-pause
|
||||
if !restart {
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
case <-t.C: // break through and retry the connection
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Stop gracefully turns the Prober off, completing the current probes before exiting.
|
||||
func (p *Prober) Stop() { close(p.pause) }
|
||||
|
||||
// Pauses the prober if currently running, or starts if it was previously paused.
|
||||
func (p *Prober) Toggle() { p.pause <- true }
|
||||
|
||||
// CurrentStatus returns the current results of the prober, regardless of whether they have
|
||||
// completed or not.
|
||||
func (p *Prober) CurrentStatus() (res ProbeResult, err error) {
|
||||
hasPMP, errPMP := p.PMP.PresentCurrent()
|
||||
res.PMP = hasPMP
|
||||
err = errPMP
|
||||
|
||||
hasUPnP, errUPnP := p.UPnP.PresentCurrent()
|
||||
res.UPnP = hasUPnP
|
||||
if err == nil {
|
||||
err = errUPnP
|
||||
}
|
||||
|
||||
hasPCP, errPCP := p.PCP.PresentCurrent()
|
||||
res.PCP = hasPCP
|
||||
if err == nil {
|
||||
err = errPCP
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Blocks until the current probe gets any result.
|
||||
func (p *Prober) StatusBlock() (res ProbeResult, err error) {
|
||||
hasPMP, errPMP := p.PMP.PresentBlock()
|
||||
res.PMP = hasPMP
|
||||
err = errPMP
|
||||
|
||||
hasUPnP, errUPnP := p.UPnP.PresentBlock()
|
||||
res.UPnP = hasUPnP
|
||||
if err == nil {
|
||||
err = errUPnP
|
||||
}
|
||||
|
||||
hasPCP, errPCP := p.PCP.PresentBlock()
|
||||
res.PCP = hasPCP
|
||||
if err == nil {
|
||||
err = errPCP
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProbeSubResult is a result for a single probing service.
|
||||
type ProbeSubResult struct {
|
||||
cond *sync.Cond
|
||||
// If this probe has finished, regardless of success or failure
|
||||
completed bool
|
||||
|
||||
// whether or not this feature is present
|
||||
present bool
|
||||
// most recent error
|
||||
err error
|
||||
|
||||
// Time we last saw the service to be available.
|
||||
sawTime time.Time
|
||||
}
|
||||
|
||||
func NewProbeSubResult() ProbeSubResult {
|
||||
return ProbeSubResult{
|
||||
cond: &sync.Cond{
|
||||
L: &sync.Mutex{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PresentBlock blocks until the probe completes, then returns the result.
|
||||
func (psr *ProbeSubResult) PresentBlock() (bool, error) {
|
||||
psr.cond.L.Lock()
|
||||
defer psr.cond.L.Unlock()
|
||||
for !psr.completed {
|
||||
psr.cond.Wait()
|
||||
}
|
||||
return psr.present, psr.err
|
||||
}
|
||||
|
||||
// PresentCurrent returns the current state, regardless whether or not the probe has completed.
|
||||
func (psr *ProbeSubResult) PresentCurrent() (bool, error) {
|
||||
psr.cond.L.Lock()
|
||||
defer psr.cond.L.Unlock()
|
||||
present := psr.present && psr.sawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
return present, psr.err
|
||||
}
|
||||
|
||||
// Assigns the result of the probe and any error seen, signalling to any items waiting for this
|
||||
// result that it is now available.
|
||||
func (psr *ProbeSubResult) Set(present bool, err error) {
|
||||
saw := time.Now()
|
||||
psr.cond.L.Lock()
|
||||
psr.sawTime = saw
|
||||
psr.completed = true
|
||||
psr.err = err
|
||||
psr.present = present
|
||||
psr.cond.L.Unlock()
|
||||
|
||||
psr.cond.Broadcast()
|
||||
}
|
||||
|
||||
func (c *Client) probePMPAndPCP(ctx context.Context) (pcp bool, pmp bool, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return false, false, ErrGatewayNotFound
|
||||
}
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP/PMP: %v", err)
|
||||
return false, false, err
|
||||
}
|
||||
defer uc.Close()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
if c.sawPMPRecently() {
|
||||
pmp = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
pcp = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && pmp {
|
||||
// Nothing more to discover.
|
||||
return
|
||||
}
|
||||
n, _, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return pcp, pmp, err
|
||||
}
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
//c.mu.Lock()
|
||||
//c.pcpSawTime = time.Now()
|
||||
//c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
pcp = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
pcp = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
pmp = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
net/portmapper/probe_test.go
Normal file
26
net/portmapper/probe_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClientProber(t *testing.T) {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(t.Logf)
|
||||
ctx := context.Background()
|
||||
prober := c.NewProber(ctx)
|
||||
time.Sleep(3 * time.Second)
|
||||
prober.Stop()
|
||||
res, err := prober.CurrentStatus()
|
||||
t.Logf("Got: %+v, %v", res, err)
|
||||
}
|
||||
202
net/portmapper/upnp.go
Normal file
202
net/portmapper/upnp.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package portmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tempfork/upnp/dcps/internetgateway2"
|
||||
)
|
||||
|
||||
type upnpMapping struct {
|
||||
gw netaddr.IP
|
||||
external netaddr.IPPort
|
||||
internal netaddr.IPPort
|
||||
useUntil time.Time
|
||||
client upnpClient
|
||||
}
|
||||
|
||||
func (u *upnpMapping) isCurrent() bool { return u.useUntil.After(time.Now()) }
|
||||
func (u *upnpMapping) validUntil() time.Time { return u.useUntil }
|
||||
func (u *upnpMapping) externalIPPort() netaddr.IPPort { return u.external }
|
||||
func (u *upnpMapping) release() {
|
||||
u.client.DeletePortMapping(context.Background(), "", u.external.Port(), "udp")
|
||||
}
|
||||
|
||||
type upnpClient interface {
|
||||
// http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
|
||||
// Implicitly assume that the calls for all these are uniform, which might be a dangerous
|
||||
// assumption.
|
||||
AddPortMapping(
|
||||
ctx context.Context,
|
||||
newRemoteHost string,
|
||||
newExternalPort uint16,
|
||||
newProtocol string,
|
||||
newInternalPort uint16,
|
||||
newInternalClient string,
|
||||
newEnabled bool,
|
||||
newPortMappingDescription string,
|
||||
newLeaseDuration uint32,
|
||||
) (err error)
|
||||
|
||||
DeletePortMapping(ctx context.Context, newRemoteHost string, newExternalPort uint16, newProtocol string) error
|
||||
GetStatusInfo(ctx context.Context) (status string, lastErr string, uptime uint32, err error)
|
||||
GetExternalIPAddress(ctx context.Context) (externalIPAddress string, err error)
|
||||
|
||||
RequestTermination(ctx context.Context) error
|
||||
RequestConnection(ctx context.Context) error
|
||||
}
|
||||
|
||||
// addAnyPortMapping abstracts over different UPnP client connections, calling the available
|
||||
// AddAnyPortMapping call if available, otherwise defaulting to the old behavior of calling
|
||||
// AddPortMapping with port = 0 to specify a wildcard port.
|
||||
func addAnyPortMapping(
|
||||
ctx context.Context,
|
||||
upnp upnpClient,
|
||||
newRemoteHost string,
|
||||
newExternalPort uint16,
|
||||
newProtocol string,
|
||||
newInternalPort uint16,
|
||||
newInternalClient string,
|
||||
newEnabled bool,
|
||||
newPortMappingDescription string,
|
||||
newLeaseDuration uint32,
|
||||
) (newPort uint16, err error) {
|
||||
if upnp, ok := upnp.(*internetgateway2.WANIPConnection2); ok {
|
||||
return upnp.AddAnyPortMapping(
|
||||
ctx,
|
||||
newRemoteHost,
|
||||
newExternalPort,
|
||||
newProtocol,
|
||||
newInternalPort,
|
||||
newInternalClient,
|
||||
newEnabled,
|
||||
newPortMappingDescription,
|
||||
newLeaseDuration,
|
||||
)
|
||||
}
|
||||
err = upnp.AddPortMapping(
|
||||
ctx,
|
||||
newRemoteHost,
|
||||
newExternalPort,
|
||||
newProtocol,
|
||||
newInternalPort,
|
||||
newInternalClient,
|
||||
newEnabled,
|
||||
newPortMappingDescription,
|
||||
newLeaseDuration,
|
||||
)
|
||||
return newInternalPort, err
|
||||
}
|
||||
|
||||
// getUPnPClients gets a client for interfacing with UPnP, ignoring the underlying protocol for
|
||||
// now.
|
||||
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
|
||||
func getUPnPClient(ctx context.Context) (upnpClient, error) {
|
||||
tasks, _ := errgroup.WithContext(ctx)
|
||||
// Attempt to connect over the multiple available connection types.
|
||||
var ip1Clients []*internetgateway2.WANIPConnection1
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip1Clients, _, err = internetgateway2.NewWANIPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
var ip2Clients []*internetgateway2.WANIPConnection2
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip2Clients, _, err = internetgateway2.NewWANIPConnection2Clients()
|
||||
return err
|
||||
})
|
||||
var ppp1Clients []*internetgateway2.WANPPPConnection1
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
|
||||
err := tasks.Wait()
|
||||
|
||||
switch {
|
||||
case len(ip2Clients) > 0:
|
||||
return ip2Clients[0], nil
|
||||
case len(ip1Clients) > 0:
|
||||
return ip1Clients[0], nil
|
||||
case len(ppp1Clients) > 0:
|
||||
return ppp1Clients[0], nil
|
||||
default:
|
||||
// Didn't get any outputs, report if there was an error or nil if
|
||||
// just no clients.
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// getUPnPPortMapping will attempt to create a port-mapping over the UPnP protocol. On success,
|
||||
// it will return the externally exposed IP and port. Otherwise, it will return a zeroed IP and
|
||||
// port and an error.
|
||||
func (c *Client) getUPnPPortMapping(ctx context.Context, gw netaddr.IP, internal netaddr.IPPort,
|
||||
prevPort uint16) (external netaddr.IPPort, err error) {
|
||||
// If did not see UPnP within the past 5 seconds then bail
|
||||
haveRecentUPnP := c.sawUPnPRecently()
|
||||
now := time.Now()
|
||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentUPnP {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
// Otherwise try a uPnP mapping if PMP did not work
|
||||
mpnp := &upnpMapping{
|
||||
gw: gw,
|
||||
internal: internal,
|
||||
}
|
||||
|
||||
var client upnpClient
|
||||
c.mu.Lock()
|
||||
oldMapping, ok := c.mapping.(*upnpMapping)
|
||||
c.mu.Unlock()
|
||||
if ok && oldMapping != nil {
|
||||
client = oldMapping.client
|
||||
} else if c.Prober != nil && c.Prober.upnpClient != nil {
|
||||
client = c.Prober.upnpClient
|
||||
} else {
|
||||
client, err = getUPnPClient(ctx)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
}
|
||||
if client == nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
var newPort uint16
|
||||
newPort, err = addAnyPortMapping(
|
||||
ctx, client,
|
||||
"", prevPort, "UDP", internal.Port(), internal.IP().String(), true,
|
||||
// string below is just a name for reporting on device.
|
||||
"tailscale-portmap", pmpMapLifetimeSec,
|
||||
)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
// TODO cache this ip somewhere?
|
||||
extIP, err := client.GetExternalIPAddress(ctx)
|
||||
if err != nil {
|
||||
// TODO this doesn't seem right
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
externalIP, err := netaddr.ParseIP(extIP)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
mpnp.external = netaddr.IPPortFrom(externalIP, newPort)
|
||||
d := time.Duration(pmpMapLifetimeSec) * time.Second / 2
|
||||
mpnp.useUntil = time.Now().Add(d)
|
||||
mpnp.client = client
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mapping = mpnp
|
||||
c.localPort = newPort
|
||||
return mpnp.external, nil
|
||||
}
|
||||
23
tempfork/upnp/LICENSE
Normal file
23
tempfork/upnp/LICENSE
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2013, John Beisley <johnbeisleyuk@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
46
tempfork/upnp/README.md
Normal file
46
tempfork/upnp/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
goupnp is a UPnP client library for Go
|
||||
|
||||
## Installation
|
||||
|
||||
Run `go get -u github.com/huin/goupnp`.
|
||||
|
||||
## Documentation
|
||||
|
||||
See [GUIDE.md](GUIDE.md) for a quick start on the most common use case for this
|
||||
library.
|
||||
|
||||
Supported DCPs (you probably want to start with one of these):
|
||||
|
||||
- [ av1](https://godoc.org/github.com/huin/goupnp/dcps/av1) - Client for UPnP Device Control Protocol MediaServer v1 and MediaRenderer v1.
|
||||
- [ internetgateway1](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway1) - Client for UPnP Device Control Protocol Internet Gateway Device v1.
|
||||
- [ internetgateway2](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway2) - Client for UPnP Device Control Protocol Internet Gateway Device v2.
|
||||
|
||||
Core components:
|
||||
|
||||
- [ (goupnp)](https://godoc.org/github.com/huin/goupnp) core library - contains datastructures and utilities typically used by the implemented DCPs.
|
||||
- [ httpu](https://godoc.org/github.com/huin/goupnp/httpu) HTTPU implementation, underlies SSDP.
|
||||
- [ ssdp](https://godoc.org/github.com/huin/goupnp/ssdp) SSDP client implementation (simple service discovery protocol) - used to discover UPnP services on a network.
|
||||
- [ soap](https://godoc.org/github.com/huin/goupnp/soap) SOAP client implementation (simple object access protocol) - used to communicate with discovered services.
|
||||
|
||||
## Regenerating dcps generated source code:
|
||||
|
||||
1. Build code generator:
|
||||
|
||||
`go get -u github.com/huin/goupnp/cmd/goupnpdcpgen`
|
||||
|
||||
2. Regenerate the code:
|
||||
|
||||
`go generate ./...`
|
||||
|
||||
## Supporting additional UPnP devices and services:
|
||||
|
||||
Supporting additional services is, in the trivial case, simply a matter of
|
||||
adding the service to the `dcpMetadata` whitelist in `cmd/goupnpdcpgen/metadata.go`,
|
||||
regenerating the source code (see above), and committing that source code.
|
||||
|
||||
However, it would be helpful if anyone needing such a service could test the
|
||||
service against the service they have, and then reporting any trouble
|
||||
encountered as an [issue on this
|
||||
project](https://github.com/huin/goupnp/issues/new). If it just works, then
|
||||
please report at least minimal working functionality as an issue, and
|
||||
optionally contribute the metadata upstream.
|
||||
154
tempfork/upnp/cmd/goupnpdcpgen/codetemplate.go
Normal file
154
tempfork/upnp/cmd/goupnpdcpgen/codetemplate.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
)
|
||||
|
||||
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
|
||||
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
|
||||
// {{if .Metadata.DocURL}}
|
||||
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
|
||||
//
|
||||
// Typically, use one of the New* functions to create clients for services.
|
||||
package {{$name}}
|
||||
|
||||
// ***********************************************************
|
||||
// GENERATED FILE - DO NOT EDIT BY HAND. See README.md
|
||||
// ***********************************************************
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tempfork/upnp"
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
// Hack to avoid Go complaining if time isn't used.
|
||||
var _ time.Time
|
||||
|
||||
// Device URNs:
|
||||
const ({{range .DeviceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
// Service URNs:
|
||||
const ({{range .ServiceTypes}}
|
||||
{{.Const}} = "{{.URN}}"{{end}}
|
||||
)
|
||||
|
||||
{{range .Services}}
|
||||
{{$srv := .}}
|
||||
{{$srvIdent := printf "%s%s" .Name .Version}}
|
||||
|
||||
// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See
|
||||
// goupnp.ServiceClient, which contains RootDevice and Service attributes which
|
||||
// are provided for informational value.
|
||||
type {{$srvIdent}} struct {
|
||||
goupnp.ServiceClient
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}Clients discovers instances of the service on the network,
|
||||
// and returns clients to any that are found. errors will contain an error for
|
||||
// any devices that replied but which could not be queried, and err will be set
|
||||
// if the discovery process failed outright.
|
||||
//
|
||||
// This is a typical entry calling point into this package.
|
||||
func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) {
|
||||
var genericClients []goupnp.ServiceClient
|
||||
if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil {
|
||||
return
|
||||
}
|
||||
clients = new{{$srvIdent}}ClientsFromGenericClients(genericClients)
|
||||
return
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsByURL discovers instances of the service at the given
|
||||
// URL, and returns clients to any that are found. An error is returned if
|
||||
// there was an error probing the service.
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered service URL.
|
||||
func New{{$srvIdent}}ClientsByURL(loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsByURL(loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
// New{{$srvIdent}}ClientsFromRootDevice discovers instances of the service in
|
||||
// a given root device, and returns clients to any that are found. An error is
|
||||
// returned if there was not at least one instance of the service within the
|
||||
// device. The location parameter is simply assigned to the Location attribute
|
||||
// of the wrapped ServiceClient(s).
|
||||
//
|
||||
// This is a typical entry calling point into this package when reusing an
|
||||
// previously discovered root device.
|
||||
func New{{$srvIdent}}ClientsFromRootDevice(rootDevice *goupnp.RootDevice, loc *url.URL) ([]*{{$srvIdent}}, error) {
|
||||
genericClients, err := goupnp.NewServiceClientsFromRootDevice(rootDevice, loc, {{$srv.Const}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
|
||||
}
|
||||
|
||||
func new{{$srvIdent}}ClientsFromGenericClients(genericClients []goupnp.ServiceClient) []*{{$srvIdent}} {
|
||||
clients := make([]*{{$srvIdent}}, len(genericClients))
|
||||
for i := range genericClients {
|
||||
clients[i] = &{{$srvIdent}}{genericClients[i]}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
|
||||
|
||||
{{$winargs := $srv.WrapArguments .InputArguments}}
|
||||
{{$woutargs := $srv.WrapArguments .OutputArguments}}
|
||||
{{if $winargs.HasDoc}}
|
||||
//
|
||||
// Arguments:{{range $winargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
{{if $woutargs.HasDoc}}
|
||||
//
|
||||
// Return values:{{range $woutargs}}{{if .HasDoc}}
|
||||
//
|
||||
// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
|
||||
func (client *{{$srvIdent}}) {{.Name}}(ctx context.Context, {{range $winargs -}}
|
||||
{{.AsParameter}}, {{end -}}
|
||||
) ({{range $woutargs -}}
|
||||
{{.AsParameter}}, {{end}} err error) {
|
||||
// Request structure.
|
||||
request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
// BEGIN Marshal arguments into request.
|
||||
{{range $winargs}}
|
||||
if request.{{.Name}}, err = {{.Marshal}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Marshal arguments into request.
|
||||
|
||||
// Response structure.
|
||||
response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
|
||||
|
||||
// Perform the SOAP call.
|
||||
if err = client.SOAPClient.PerformAction(ctx, {{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// BEGIN Unmarshal arguments from response.
|
||||
{{range $woutargs}}
|
||||
if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil {
|
||||
return
|
||||
}{{end}}
|
||||
// END Unmarshal arguments from response.
|
||||
return
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "argstruct"}}struct {{"{"}}
|
||||
{{range .}}{{.Name}} string
|
||||
{{end}}{{"}"}}{{end}}
|
||||
`))
|
||||
265
tempfork/upnp/cmd/goupnpdcpgen/dcp.go
Normal file
265
tempfork/upnp/cmd/goupnpdcpgen/dcp.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tempfork/upnp"
|
||||
"tailscale.com/tempfork/upnp/scpd"
|
||||
)
|
||||
|
||||
// DCP collects together information about a UPnP Device Control Protocol.
|
||||
type DCP struct {
|
||||
Metadata DCPMetadata
|
||||
DeviceTypes map[string]*URNParts
|
||||
ServiceTypes map[string]*URNParts
|
||||
Services []SCPDWithURN
|
||||
}
|
||||
|
||||
func newDCP(metadata DCPMetadata) *DCP {
|
||||
return &DCP{
|
||||
Metadata: metadata,
|
||||
DeviceTypes: make(map[string]*URNParts),
|
||||
ServiceTypes: make(map[string]*URNParts),
|
||||
}
|
||||
}
|
||||
|
||||
func (dcp *DCP) processZipFile(filename string) error {
|
||||
archive, err := zip.OpenReader(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading zip file %q: %v", filename, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
for _, deviceFile := range globFiles("*/device/*.xml", archive) {
|
||||
if err := dcp.processDeviceFile(deviceFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
|
||||
if err := dcp.processSCPDFile(scpdFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dcp *DCP) processDeviceFile(file *zip.File) error {
|
||||
var device goupnp.Device
|
||||
if err := unmarshalXmlFile(file, &device); err != nil {
|
||||
return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
var mainErr error
|
||||
device.VisitDevices(func(d *goupnp.Device) {
|
||||
t := strings.TrimSpace(d.DeviceType)
|
||||
if t != "" {
|
||||
u, err := extractURNParts(t, deviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.DeviceTypes[t] = u
|
||||
}
|
||||
})
|
||||
device.VisitServices(func(s *goupnp.Service) {
|
||||
u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
|
||||
if err != nil {
|
||||
mainErr = err
|
||||
}
|
||||
dcp.ServiceTypes[s.ServiceType] = u
|
||||
})
|
||||
return mainErr
|
||||
}
|
||||
|
||||
func (dcp *DCP) writeCode(outFile string, useGofmt bool) error {
|
||||
packageFile, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var output io.WriteCloser = packageFile
|
||||
if useGofmt {
|
||||
if output, err = NewGofmtWriteCloser(output); err != nil {
|
||||
packageFile.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = packageTmpl.Execute(output, dcp); err != nil {
|
||||
output.Close()
|
||||
return err
|
||||
}
|
||||
return output.Close()
|
||||
}
|
||||
|
||||
func (dcp *DCP) processSCPDFile(file *zip.File) error {
|
||||
scpd := new(scpd.SCPD)
|
||||
if err := unmarshalXmlFile(file, scpd); err != nil {
|
||||
return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
|
||||
}
|
||||
scpd.Clean()
|
||||
urnParts, err := urnPartsFromSCPDFilename(file.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
|
||||
}
|
||||
dcp.Services = append(dcp.Services, SCPDWithURN{
|
||||
URNParts: urnParts,
|
||||
SCPD: scpd,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type SCPDWithURN struct {
|
||||
*URNParts
|
||||
SCPD *scpd.SCPD
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) WrapArguments(args []*scpd.Argument) (argumentWrapperList, error) {
|
||||
wrappedArgs := make(argumentWrapperList, len(args))
|
||||
for i, arg := range args {
|
||||
wa, err := s.wrapArgument(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrappedArgs[i] = wa
|
||||
}
|
||||
return wrappedArgs, nil
|
||||
}
|
||||
|
||||
func (s *SCPDWithURN) wrapArgument(arg *scpd.Argument) (*argumentWrapper, error) {
|
||||
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
|
||||
if relVar == nil {
|
||||
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
cnv, ok := typeConvs[relVar.DataType.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
|
||||
}
|
||||
return &argumentWrapper{
|
||||
Argument: *arg,
|
||||
relVar: relVar,
|
||||
conv: cnv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type argumentWrapper struct {
|
||||
scpd.Argument
|
||||
relVar *scpd.StateVariable
|
||||
conv conv
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) AsParameter() string {
|
||||
return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) HasDoc() bool {
|
||||
rng := arg.relVar.AllowedValueRange
|
||||
return ((rng != nil && (rng.Minimum != "" || rng.Maximum != "" || rng.Step != "")) ||
|
||||
len(arg.relVar.AllowedValues) > 0)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Document() string {
|
||||
relVar := arg.relVar
|
||||
if rng := relVar.AllowedValueRange; rng != nil {
|
||||
var parts []string
|
||||
if rng.Minimum != "" {
|
||||
parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum))
|
||||
}
|
||||
if rng.Maximum != "" {
|
||||
parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum))
|
||||
}
|
||||
if rng.Step != "" {
|
||||
parts = append(parts, fmt.Sprintf("step=%s", rng.Step))
|
||||
}
|
||||
return "allowed value range: " + strings.Join(parts, ", ")
|
||||
}
|
||||
if len(relVar.AllowedValues) != 0 {
|
||||
return "allowed values: " + strings.Join(relVar.AllowedValues, ", ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Marshal() string {
|
||||
return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name)
|
||||
}
|
||||
|
||||
func (arg *argumentWrapper) Unmarshal(objVar string) string {
|
||||
return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name)
|
||||
}
|
||||
|
||||
type argumentWrapperList []*argumentWrapper
|
||||
|
||||
func (args argumentWrapperList) HasDoc() bool {
|
||||
for _, arg := range args {
|
||||
if arg.HasDoc() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type URNParts struct {
|
||||
URN string
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (u *URNParts) Const() string {
|
||||
return fmt.Sprintf("URN_%s_%s", u.Name, u.Version)
|
||||
}
|
||||
|
||||
// extractURNParts extracts the name and version from a URN string.
|
||||
func extractURNParts(urn, expectedPrefix string) (*URNParts, error) {
|
||||
if !strings.HasPrefix(urn, expectedPrefix) {
|
||||
return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix)
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("%q does not have a name and version", urn)
|
||||
}
|
||||
name, version := parts[0], parts[1]
|
||||
return &URNParts{urn, name, version}, nil
|
||||
}
|
||||
|
||||
// Taken from: https://github.com/huin/goutil/blob/master/codegen/gofmt.go
|
||||
// License: https://github.com/huin/goutil/blob/master/LICENSE
|
||||
// NewGofmtWriteCloser returns an io.WriteCloser that filters what is written
|
||||
// to it through gofmt. It must be closed for this process to be completed, an
|
||||
// error from Close can be due to syntax errors in the source that has been
|
||||
// written.
|
||||
type goFmtWriteCloser struct {
|
||||
output io.WriteCloser
|
||||
stdin io.WriteCloser
|
||||
gofmt *exec.Cmd
|
||||
}
|
||||
|
||||
func NewGofmtWriteCloser(output io.WriteCloser) (io.WriteCloser, error) {
|
||||
gofmt := exec.Command("gofmt")
|
||||
gofmt.Stdout = output
|
||||
gofmt.Stderr = os.Stderr
|
||||
stdin, err := gofmt.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = gofmt.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &goFmtWriteCloser{
|
||||
output: output,
|
||||
stdin: stdin,
|
||||
gofmt: gofmt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gwc *goFmtWriteCloser) Write(p []byte) (int, error) {
|
||||
return gwc.stdin.Write(p)
|
||||
}
|
||||
|
||||
func (gwc *goFmtWriteCloser) Close() error {
|
||||
gwc.stdin.Close()
|
||||
if err := gwc.output.Close(); err != nil {
|
||||
gwc.gofmt.Wait()
|
||||
return err
|
||||
}
|
||||
return gwc.gofmt.Wait()
|
||||
}
|
||||
88
tempfork/upnp/cmd/goupnpdcpgen/fileutil.go
Normal file
88
tempfork/upnp/cmd/goupnpdcpgen/fileutil.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func acquireFile(specFilename string, xmlSpecURL string) error {
|
||||
if f, err := os.Open(specFilename); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := http.Get(xmlSpecURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("could not download spec %q from %q: %s",
|
||||
specFilename, xmlSpecURL, resp.Status)
|
||||
}
|
||||
|
||||
tmpFilename := specFilename + ".download"
|
||||
w, err := os.Create(tmpFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpFilename, specFilename)
|
||||
}
|
||||
|
||||
func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
|
||||
var files []*zip.File
|
||||
for _, f := range archive.File {
|
||||
if matched, err := path.Match(pattern, f.Name); err != nil {
|
||||
// This shouldn't happen - all patterns are hard-coded, errors in them
|
||||
// are a programming error.
|
||||
panic(err)
|
||||
} else if matched {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func unmarshalXmlFile(file *zip.File, data interface{}) error {
|
||||
r, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := xml.NewDecoder(r)
|
||||
defer r.Close()
|
||||
return decoder.Decode(data)
|
||||
}
|
||||
|
||||
var scpdFilenameRe = regexp.MustCompile(
|
||||
`.*/([a-zA-Z0-9]+)([0-9]+)\.xml`)
|
||||
|
||||
func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
|
||||
parts := scpdFilenameRe.FindStringSubmatch(filename)
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename)
|
||||
}
|
||||
name, version := parts[1], parts[2]
|
||||
return &URNParts{
|
||||
URN: serviceURNPrefix + name + ":" + version,
|
||||
Name: name,
|
||||
Version: version,
|
||||
}, nil
|
||||
}
|
||||
64
tempfork/upnp/cmd/goupnpdcpgen/goupnpdcpgen.go
Normal file
64
tempfork/upnp/cmd/goupnpdcpgen/goupnpdcpgen.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Command to generate DCP package source from the XML specification.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
deviceURNPrefix = "urn:schemas-upnp-org:device:"
|
||||
serviceURNPrefix = "urn:schemas-upnp-org:service:"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
dcpName = flag.String("dcp_name", "", "Name of the DCP to generate.")
|
||||
specsDir = flag.String("specs_dir", ".", "Path to the specification storage directory. "+
|
||||
"This is used to find (and download if not present) the specification ZIP files.")
|
||||
useGofmt = flag.Bool("gofmt", true, "Pass the generated code through gofmt. "+
|
||||
"Disable this if debugging code generation and needing to see the generated code "+
|
||||
"prior to being passed through gofmt.")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if err := run(*dcpName, *specsDir, *useGofmt); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(dcpName, specsDir string, useGofmt bool) error {
|
||||
if err := os.MkdirAll(specsDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("could not create specs-dir %q: %v", specsDir, err)
|
||||
}
|
||||
|
||||
for _, d := range dcpMetadata {
|
||||
if d.Name != dcpName {
|
||||
continue
|
||||
}
|
||||
specFilename := filepath.Join(specsDir, d.Name+".zip")
|
||||
err := acquireFile(specFilename, d.XMLSpecURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not acquire spec for %s: %v", d.Name, err)
|
||||
}
|
||||
dcp := newDCP(d)
|
||||
if err := dcp.processZipFile(specFilename); err != nil {
|
||||
return fmt.Errorf("error processing spec for %s in file %q: %v", d.Name, specFilename, err)
|
||||
}
|
||||
for i, hack := range d.Hacks {
|
||||
if err := hack(dcp); err != nil {
|
||||
return fmt.Errorf("error with Hack[%d] for %s: %v", i, d.Name, err)
|
||||
}
|
||||
}
|
||||
if err := dcp.writeCode(d.Name+".go", useGofmt); err != nil {
|
||||
return fmt.Errorf("error writing package %q: %v", dcp.Metadata.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not find DCP with name %q", dcpName)
|
||||
}
|
||||
83
tempfork/upnp/cmd/goupnpdcpgen/metadata.go
Normal file
83
tempfork/upnp/cmd/goupnpdcpgen/metadata.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
// DCP contains extra metadata to use when generating DCP source files.
|
||||
type DCPMetadata struct {
|
||||
Name string // What to name the Go DCP package.
|
||||
OfficialName string // Official name for the DCP.
|
||||
DocURL string // Optional - URL for further documentation about the DCP.
|
||||
XMLSpecURL string // Where to download the XML spec from.
|
||||
// Any special-case functions to run against the DCP before writing it out.
|
||||
Hacks []DCPHackFn
|
||||
}
|
||||
|
||||
var dcpMetadata = []DCPMetadata{
|
||||
{
|
||||
Name: "internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
|
||||
Hacks: []DCPHackFn{
|
||||
func(dcp *DCP) error {
|
||||
missingURN := "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
|
||||
if _, ok := dcp.ServiceTypes[missingURN]; ok {
|
||||
return nil
|
||||
}
|
||||
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
return nil
|
||||
}, totalBytesHack,
|
||||
func(dcp *DCP) error {
|
||||
// omit certain device types that we do not need
|
||||
var allowedServices = map[string]bool{
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:1": true,
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:2": true,
|
||||
"urn:schemas-upnp-org:service:WANPPPConnection:1": true,
|
||||
}
|
||||
var allowedParts = map[string]bool{
|
||||
"WANIPConnection": true,
|
||||
"WANPPPConnection": true,
|
||||
}
|
||||
for service := range dcp.ServiceTypes {
|
||||
if _, ok := allowedServices[service]; ok {
|
||||
continue
|
||||
}
|
||||
delete(dcp.ServiceTypes, service)
|
||||
}
|
||||
var permitted []SCPDWithURN
|
||||
for _, v := range dcp.Services {
|
||||
if _, ok := allowedParts[v.URNParts.Name]; ok {
|
||||
permitted = append(permitted, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
dcp.Services = permitted
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func totalBytesHack(dcp *DCP) error {
|
||||
for _, service := range dcp.Services {
|
||||
if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" {
|
||||
variables := service.SCPD.StateVariables
|
||||
for key, variable := range variables {
|
||||
varName := variable.Name
|
||||
if varName == "TotalBytesSent" || varName == "TotalBytesReceived" {
|
||||
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
|
||||
variable.DataType.Name = "ui8"
|
||||
variables[key] = variable
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DCPHackFn func(*DCP) error
|
||||
35
tempfork/upnp/cmd/goupnpdcpgen/typemap.go
Normal file
35
tempfork/upnp/cmd/goupnpdcpgen/typemap.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
type conv struct {
|
||||
FuncSuffix string
|
||||
ExtType string
|
||||
}
|
||||
|
||||
// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name
|
||||
// suffix inside the soap module (e.g "Fixed14_4") and the Go type.
|
||||
var typeConvs = map[string]conv{
|
||||
"ui1": {"Ui1", "uint8"},
|
||||
"ui2": {"Ui2", "uint16"},
|
||||
"ui4": {"Ui4", "uint32"},
|
||||
"ui8": {"Ui8", "uint64"},
|
||||
"i1": {"I1", "int8"},
|
||||
"i2": {"I2", "int16"},
|
||||
"i4": {"I4", "int32"},
|
||||
"int": {"Int", "int64"},
|
||||
"r4": {"R4", "float32"},
|
||||
"r8": {"R8", "float64"},
|
||||
"number": {"R8", "float64"}, // Alias for r8.
|
||||
"fixed.14.4": {"Fixed14_4", "float64"},
|
||||
"float": {"R8", "float64"},
|
||||
"char": {"Char", "rune"},
|
||||
"string": {"String", "string"},
|
||||
"date": {"Date", "time.Time"},
|
||||
"dateTime": {"DateTime", "time.Time"},
|
||||
"dateTime.tz": {"DateTimeTz", "time.Time"},
|
||||
"time": {"TimeOfDay", "soap.TimeOfDay"},
|
||||
"time.tz": {"TimeOfDayTz", "soap.TimeOfDay"},
|
||||
"boolean": {"Boolean", "bool"},
|
||||
"bin.base64": {"BinBase64", "[]byte"},
|
||||
"bin.hex": {"BinHex", "[]byte"},
|
||||
"uri": {"URI", "*url.URL"},
|
||||
}
|
||||
1
tempfork/upnp/dcps/internetgateway2/.gitignore
vendored
Normal file
1
tempfork/upnp/dcps/internetgateway2/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.zip
|
||||
2
tempfork/upnp/dcps/internetgateway2/gen.go
Normal file
2
tempfork/upnp/dcps/internetgateway2/gen.go
Normal file
@@ -0,0 +1,2 @@
|
||||
//go:generate goupnpdcpgen -dcp_name internetgateway2
|
||||
package internetgateway2
|
||||
2351
tempfork/upnp/dcps/internetgateway2/internetgateway2.go
Normal file
2351
tempfork/upnp/dcps/internetgateway2/internetgateway2.go
Normal file
File diff suppressed because it is too large
Load Diff
190
tempfork/upnp/device.go
Normal file
190
tempfork/upnp/device.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// This file contains XML structures for communicating with UPnP devices.
|
||||
|
||||
package goupnp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/tempfork/upnp/scpd"
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceXMLNamespace = "urn:schemas-upnp-org:device-1-0"
|
||||
)
|
||||
|
||||
// RootDevice is the device description as described by section 2.3 "Device
|
||||
// description" in
|
||||
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
||||
type RootDevice struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
SpecVersion SpecVersion `xml:"specVersion"`
|
||||
URLBase url.URL `xml:"-"`
|
||||
URLBaseStr string `xml:"URLBase"`
|
||||
Device Device `xml:"device"`
|
||||
}
|
||||
|
||||
// SetURLBase sets the URLBase for the RootDevice and its underlying components.
|
||||
func (root *RootDevice) SetURLBase(urlBase *url.URL) {
|
||||
root.URLBase = *urlBase
|
||||
root.URLBaseStr = urlBase.String()
|
||||
root.Device.SetURLBase(urlBase)
|
||||
}
|
||||
|
||||
// SpecVersion is part of a RootDevice, describes the version of the
|
||||
// specification that the data adheres to.
|
||||
type SpecVersion struct {
|
||||
Major int32 `xml:"major"`
|
||||
Minor int32 `xml:"minor"`
|
||||
}
|
||||
|
||||
// Device is a UPnP device. It can have child devices.
|
||||
type Device struct {
|
||||
DeviceType string `xml:"deviceType"`
|
||||
FriendlyName string `xml:"friendlyName"`
|
||||
Manufacturer string `xml:"manufacturer"`
|
||||
ManufacturerURL URLField `xml:"manufacturerURL"`
|
||||
ModelDescription string `xml:"modelDescription"`
|
||||
ModelName string `xml:"modelName"`
|
||||
ModelNumber string `xml:"modelNumber"`
|
||||
ModelURL URLField `xml:"modelURL"`
|
||||
SerialNumber string `xml:"serialNumber"`
|
||||
UDN string `xml:"UDN"`
|
||||
UPC string `xml:"UPC,omitempty"`
|
||||
Icons []Icon `xml:"iconList>icon,omitempty"`
|
||||
Services []Service `xml:"serviceList>service,omitempty"`
|
||||
Devices []Device `xml:"deviceList>device,omitempty"`
|
||||
|
||||
// Extra observed elements:
|
||||
PresentationURL URLField `xml:"presentationURL"`
|
||||
}
|
||||
|
||||
// VisitDevices calls visitor for the device, and all its descendent devices.
|
||||
func (device *Device) VisitDevices(visitor func(*Device)) {
|
||||
visitor(device)
|
||||
for i := range device.Devices {
|
||||
device.Devices[i].VisitDevices(visitor)
|
||||
}
|
||||
}
|
||||
|
||||
// VisitServices calls visitor for all Services under the device and all its
|
||||
// descendent devices.
|
||||
func (device *Device) VisitServices(visitor func(*Service)) {
|
||||
device.VisitDevices(func(d *Device) {
|
||||
for i := range d.Services {
|
||||
visitor(&d.Services[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FindService finds all (if any) Services under the device and its descendents
|
||||
// that have the given ServiceType.
|
||||
func (device *Device) FindService(serviceType string) []*Service {
|
||||
var services []*Service
|
||||
device.VisitServices(func(s *Service) {
|
||||
if s.ServiceType == serviceType {
|
||||
services = append(services, s)
|
||||
}
|
||||
})
|
||||
return services
|
||||
}
|
||||
|
||||
// SetURLBase sets the URLBase for the Device and its underlying components.
|
||||
func (device *Device) SetURLBase(urlBase *url.URL) {
|
||||
device.ManufacturerURL.SetURLBase(urlBase)
|
||||
device.ModelURL.SetURLBase(urlBase)
|
||||
device.PresentationURL.SetURLBase(urlBase)
|
||||
for i := range device.Icons {
|
||||
device.Icons[i].SetURLBase(urlBase)
|
||||
}
|
||||
for i := range device.Services {
|
||||
device.Services[i].SetURLBase(urlBase)
|
||||
}
|
||||
for i := range device.Devices {
|
||||
device.Devices[i].SetURLBase(urlBase)
|
||||
}
|
||||
}
|
||||
|
||||
func (device *Device) String() string {
|
||||
return fmt.Sprintf("Device ID %s : %s (%s)", device.UDN, device.DeviceType, device.FriendlyName)
|
||||
}
|
||||
|
||||
// Icon is a representative image that a device might include in its
|
||||
// description.
|
||||
type Icon struct {
|
||||
Mimetype string `xml:"mimetype"`
|
||||
Width int32 `xml:"width"`
|
||||
Height int32 `xml:"height"`
|
||||
Depth int32 `xml:"depth"`
|
||||
URL URLField `xml:"url"`
|
||||
}
|
||||
|
||||
// SetURLBase sets the URLBase for the Icon.
|
||||
func (icon *Icon) SetURLBase(url *url.URL) {
|
||||
icon.URL.SetURLBase(url)
|
||||
}
|
||||
|
||||
// Service is a service provided by a UPnP Device.
|
||||
type Service struct {
|
||||
ServiceType string `xml:"serviceType"`
|
||||
ServiceId string `xml:"serviceId"`
|
||||
SCPDURL URLField `xml:"SCPDURL"`
|
||||
ControlURL URLField `xml:"controlURL"`
|
||||
EventSubURL URLField `xml:"eventSubURL"`
|
||||
}
|
||||
|
||||
// SetURLBase sets the URLBase for the Service.
|
||||
func (srv *Service) SetURLBase(urlBase *url.URL) {
|
||||
srv.SCPDURL.SetURLBase(urlBase)
|
||||
srv.ControlURL.SetURLBase(urlBase)
|
||||
srv.EventSubURL.SetURLBase(urlBase)
|
||||
}
|
||||
|
||||
func (srv *Service) String() string {
|
||||
return fmt.Sprintf("Service ID %s : %s", srv.ServiceId, srv.ServiceType)
|
||||
}
|
||||
|
||||
// RequestSCPD requests the SCPD (soap actions and state variables description)
|
||||
// for the service.
|
||||
func (srv *Service) RequestSCPD() (*scpd.SCPD, error) {
|
||||
if !srv.SCPDURL.Ok {
|
||||
return nil, errors.New("bad/missing SCPD URL, or no URLBase has been set")
|
||||
}
|
||||
s := new(scpd.SCPD)
|
||||
if err := requestXml(srv.SCPDURL.URL.String(), scpd.SCPDXMLNamespace, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RequestSCDP is for compatibility only, prefer RequestSCPD. This was a
|
||||
// misspelling of RequestSCDP.
|
||||
func (srv *Service) RequestSCDP() (*scpd.SCPD, error) {
|
||||
return srv.RequestSCPD()
|
||||
}
|
||||
|
||||
func (srv *Service) NewSOAPClient() *soap.SOAPClient {
|
||||
return soap.NewSOAPClient(srv.ControlURL.URL)
|
||||
}
|
||||
|
||||
// URLField is a URL that is part of a device description.
|
||||
type URLField struct {
|
||||
URL url.URL `xml:"-"`
|
||||
Ok bool `xml:"-"`
|
||||
Str string `xml:",chardata"`
|
||||
}
|
||||
|
||||
func (uf *URLField) SetURLBase(urlBase *url.URL) {
|
||||
refUrl, err := url.Parse(uf.Str)
|
||||
if err != nil {
|
||||
uf.URL = url.URL{}
|
||||
uf.Ok = false
|
||||
return
|
||||
}
|
||||
|
||||
uf.URL = *urlBase.ResolveReference(refUrl)
|
||||
uf.Ok = true
|
||||
}
|
||||
146
tempfork/upnp/goupnp.go
Normal file
146
tempfork/upnp/goupnp.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// goupnp is an implementation of a client for various UPnP services.
|
||||
//
|
||||
// For most uses, it is recommended to use the code-generated packages under
|
||||
// github.com/huin/goupnp/dcps. Example use is shown at
|
||||
// http://godoc.org/github.com/huin/goupnp/example
|
||||
//
|
||||
// A commonly used client is internetgateway1.WANPPPConnection1:
|
||||
// http://godoc.org/github.com/huin/goupnp/dcps/internetgateway1#WANPPPConnection1
|
||||
//
|
||||
// Currently only a couple of schemas have code generated for them from the
|
||||
// UPnP example XML specifications. Not all methods will work on these clients,
|
||||
// because the generated stubs contain the full set of specified methods from
|
||||
// the XML specifications, and the discovered services will likely support a
|
||||
// subset of those methods.
|
||||
package goupnp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tempfork/upnp/ssdp"
|
||||
)
|
||||
|
||||
// ContextError is an error that wraps an error with some context information.
|
||||
type ContextError struct {
|
||||
Context string
|
||||
Err error
|
||||
}
|
||||
|
||||
func ctxError(err error, msg string) ContextError {
|
||||
return ContextError{
|
||||
Context: msg,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func ctxErrorf(err error, msg string, args ...interface{}) ContextError {
|
||||
return ContextError{
|
||||
Context: fmt.Sprintf(msg, args...),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (err ContextError) Error() string {
|
||||
return fmt.Sprintf("%s: %v", err.Context, err.Err)
|
||||
}
|
||||
|
||||
// MaybeRootDevice contains either a RootDevice or an error.
|
||||
type MaybeRootDevice struct {
|
||||
// Identifier of the device.
|
||||
USN string
|
||||
|
||||
// Set iff Err == nil.
|
||||
Root *RootDevice
|
||||
|
||||
// The location the device was discovered at. This can be used with
|
||||
// DeviceByURL, assuming the device is still present. A location represents
|
||||
// the discovery of a device, regardless of if there was an error probing it.
|
||||
Location *url.URL
|
||||
|
||||
// Any error encountered probing a discovered device.
|
||||
Err error
|
||||
}
|
||||
|
||||
// DiscoverDevices attempts to find targets of the given type. This is
|
||||
// typically the entry-point for this package. searchTarget is typically a URN
|
||||
// in the form "urn:schemas-upnp-org:device:..." or
|
||||
// "urn:schemas-upnp-org:service:...". A single error is returned for errors
|
||||
// while attempting to send the query. An error or RootDevice is returned for
|
||||
// each discovered RootDevice.
|
||||
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
|
||||
hc, hcCleanup, err := httpuClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer hcCleanup()
|
||||
responses, err := ssdp.SSDPRawSearch(hc, string(searchTarget), 2, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]MaybeRootDevice, len(responses))
|
||||
for i, response := range responses {
|
||||
maybe := &results[i]
|
||||
maybe.USN = response.Header.Get("USN")
|
||||
loc, err := response.Location()
|
||||
if err != nil {
|
||||
maybe.Err = ContextError{"unexpected bad location from search", err}
|
||||
continue
|
||||
}
|
||||
maybe.Location = loc
|
||||
if root, err := DeviceByURL(loc); err != nil {
|
||||
maybe.Err = err
|
||||
} else {
|
||||
maybe.Root = root
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func DeviceByURL(loc *url.URL) (*RootDevice, error) {
|
||||
locStr := loc.String()
|
||||
root := new(RootDevice)
|
||||
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil {
|
||||
return nil, ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
|
||||
}
|
||||
var urlBaseStr string
|
||||
if root.URLBaseStr != "" {
|
||||
urlBaseStr = root.URLBaseStr
|
||||
} else {
|
||||
urlBaseStr = locStr
|
||||
}
|
||||
urlBase, err := url.Parse(urlBaseStr)
|
||||
if err != nil {
|
||||
return nil, ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
|
||||
}
|
||||
root.SetURLBase(urlBase)
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func requestXml(url string, defaultSpace string, doc interface{}) error {
|
||||
timeout := time.Duration(3 * time.Second)
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("goupnp: got response status %s from %q",
|
||||
resp.Status, url)
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(resp.Body)
|
||||
decoder.DefaultSpace = defaultSpace
|
||||
//decoder.CharsetReader = charset.NewReaderLabel
|
||||
|
||||
return decoder.Decode(doc)
|
||||
}
|
||||
151
tempfork/upnp/httpu/httpu.go
Normal file
151
tempfork/upnp/httpu/httpu.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package httpu
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClientInterface is the general interface provided to perform HTTP-over-UDP
|
||||
// requests.
|
||||
type ClientInterface interface {
|
||||
// Do performs a request. The timeout is how long to wait for before returning
|
||||
// the responses that were received. An error is only returned for failing to
|
||||
// send the request. Failures in receipt simply do not add to the resulting
|
||||
// responses.
|
||||
Do(
|
||||
req *http.Request,
|
||||
timeout time.Duration,
|
||||
numSends int,
|
||||
) ([]*http.Response, error)
|
||||
}
|
||||
|
||||
// HTTPUClient is a client for dealing with HTTPU (HTTP over UDP). Its typical
|
||||
// function is for HTTPMU, and particularly SSDP.
|
||||
type HTTPUClient struct {
|
||||
connLock sync.Mutex // Protects use of conn.
|
||||
conn net.PacketConn
|
||||
}
|
||||
|
||||
var _ ClientInterface = &HTTPUClient{}
|
||||
|
||||
// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
|
||||
// purpose.
|
||||
func NewHTTPUClient() (*HTTPUClient, error) {
|
||||
conn, err := net.ListenPacket("udp", ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPUClient{conn: conn}, nil
|
||||
}
|
||||
|
||||
// NewHTTPUClientAddr creates a new HTTPUClient which will broadcast packets
|
||||
// from the specified address, opening up a new UDP socket for the purpose
|
||||
func NewHTTPUClientAddr(addr string) (*HTTPUClient, error) {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
return nil, errors.New("invalid listening address")
|
||||
}
|
||||
conn, err := net.ListenPacket("udp", ip.String()+":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HTTPUClient{conn: conn}, nil
|
||||
}
|
||||
|
||||
// Close shuts down the client. The client will no longer be useful following
|
||||
// this.
|
||||
func (httpu *HTTPUClient) Close() error {
|
||||
httpu.connLock.Lock()
|
||||
defer httpu.connLock.Unlock()
|
||||
return httpu.conn.Close()
|
||||
}
|
||||
|
||||
// Do implements ClientInterface.Do.
|
||||
//
|
||||
// Note that at present only one concurrent connection will happen per
|
||||
// HTTPUClient.
|
||||
func (httpu *HTTPUClient) Do(
|
||||
req *http.Request,
|
||||
timeout time.Duration,
|
||||
numSends int,
|
||||
) ([]*http.Response, error) {
|
||||
httpu.connLock.Lock()
|
||||
defer httpu.connLock.Unlock()
|
||||
|
||||
// Create the request. This is a subset of what http.Request.Write does
|
||||
// deliberately to avoid creating extra fields which may confuse some
|
||||
// devices.
|
||||
var requestBuf bytes.Buffer
|
||||
method := req.Method
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
if _, err := fmt.Fprintf(&requestBuf, "%s %s HTTP/1.1\r\n", method, req.URL.RequestURI()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Header.Write(&requestBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := requestBuf.Write([]byte{'\r', '\n'}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
destAddr, err := net.ResolveUDPAddr("udp", req.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = httpu.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send request.
|
||||
for i := 0; i < numSends; i++ {
|
||||
if n, err := httpu.conn.WriteTo(requestBuf.Bytes(), destAddr); err != nil {
|
||||
return nil, err
|
||||
} else if n < len(requestBuf.Bytes()) {
|
||||
return nil, fmt.Errorf("httpu: wrote %d bytes rather than full %d in request",
|
||||
n, len(requestBuf.Bytes()))
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Await responses until timeout.
|
||||
var responses []*http.Response
|
||||
responseBytes := make([]byte, 2048)
|
||||
for {
|
||||
// 2048 bytes should be sufficient for most networks.
|
||||
n, _, err := httpu.conn.ReadFrom(responseBytes)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok {
|
||||
if err.Timeout() {
|
||||
break
|
||||
}
|
||||
if err.Temporary() {
|
||||
// Sleep in case this is a persistent error to avoid pegging CPU until deadline.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse response.
|
||||
response, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(responseBytes[:n])), req)
|
||||
if err != nil {
|
||||
log.Printf("httpu: error while parsing response: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
responses = append(responses, response)
|
||||
}
|
||||
|
||||
// Timeout reached - return discovered responses.
|
||||
return responses, nil
|
||||
}
|
||||
70
tempfork/upnp/httpu/multiclient.go
Normal file
70
tempfork/upnp/httpu/multiclient.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package httpu
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// MultiClient dispatches requests out to all the delegated clients.
|
||||
type MultiClient struct {
|
||||
// The HTTPU clients to delegate to.
|
||||
delegates []ClientInterface
|
||||
}
|
||||
|
||||
var _ ClientInterface = &MultiClient{}
|
||||
|
||||
// NewMultiClient creates a new MultiClient that delegates to all the given
|
||||
// clients.
|
||||
func NewMultiClient(delegates []ClientInterface) *MultiClient {
|
||||
return &MultiClient{
|
||||
delegates: delegates,
|
||||
}
|
||||
}
|
||||
|
||||
// Do implements ClientInterface.Do.
|
||||
func (mc *MultiClient) Do(
|
||||
req *http.Request,
|
||||
timeout time.Duration,
|
||||
numSends int,
|
||||
) ([]*http.Response, error) {
|
||||
tasks := &errgroup.Group{}
|
||||
|
||||
results := make(chan []*http.Response)
|
||||
tasks.Go(func() error {
|
||||
defer close(results)
|
||||
return mc.sendRequests(results, req, timeout, numSends)
|
||||
})
|
||||
|
||||
var responses []*http.Response
|
||||
tasks.Go(func() error {
|
||||
for rs := range results {
|
||||
responses = append(responses, rs...)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return responses, tasks.Wait()
|
||||
}
|
||||
|
||||
func (mc *MultiClient) sendRequests(
|
||||
results chan<- []*http.Response,
|
||||
req *http.Request,
|
||||
timeout time.Duration,
|
||||
numSends int,
|
||||
) error {
|
||||
tasks := &errgroup.Group{}
|
||||
for _, d := range mc.delegates {
|
||||
d := d // copy for closure
|
||||
tasks.Go(func() error {
|
||||
responses, err := d.Do(req, timeout, numSends)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results <- responses
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return tasks.Wait()
|
||||
}
|
||||
108
tempfork/upnp/httpu/serve.go
Normal file
108
tempfork/upnp/httpu/serve.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package httpu
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaxMessageBytes = 2048
|
||||
)
|
||||
|
||||
var (
|
||||
trailingWhitespaceRx = regexp.MustCompile(" +\r\n")
|
||||
crlf = []byte("\r\n")
|
||||
)
|
||||
|
||||
// Handler is the interface by which received HTTPU messages are passed to
|
||||
// handling code.
|
||||
type Handler interface {
|
||||
// ServeMessage is called for each HTTPU message received. peerAddr contains
|
||||
// the address that the message was received from.
|
||||
ServeMessage(r *http.Request)
|
||||
}
|
||||
|
||||
// HandlerFunc is a function-to-Handler adapter.
|
||||
type HandlerFunc func(r *http.Request)
|
||||
|
||||
func (f HandlerFunc) ServeMessage(r *http.Request) {
|
||||
f(r)
|
||||
}
|
||||
|
||||
// A Server defines parameters for running an HTTPU server.
|
||||
type Server struct {
|
||||
Addr string // UDP address to listen on
|
||||
Multicast bool // Should listen for multicast?
|
||||
Interface *net.Interface // Network interface to listen on for multicast, nil for default multicast interface
|
||||
Handler Handler // handler to invoke
|
||||
MaxMessageBytes int // maximum number of bytes to read from a packet, DefaultMaxMessageBytes if 0
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the UDP network address srv.Addr. If srv.Multicast
|
||||
// is true, then a multicast UDP listener will be used on srv.Interface (or
|
||||
// default interface if nil).
|
||||
func (srv *Server) ListenAndServe() error {
|
||||
var err error
|
||||
|
||||
var addr *net.UDPAddr
|
||||
if addr, err = net.ResolveUDPAddr("udp", srv.Addr); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var conn net.PacketConn
|
||||
if srv.Multicast {
|
||||
if conn, err = net.ListenMulticastUDP("udp", srv.Interface, addr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if conn, err = net.ListenUDP("udp", addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return srv.Serve(conn)
|
||||
}
|
||||
|
||||
// Serve messages received on the given packet listener to the srv.Handler.
|
||||
func (srv *Server) Serve(l net.PacketConn) error {
|
||||
maxMessageBytes := DefaultMaxMessageBytes
|
||||
if srv.MaxMessageBytes != 0 {
|
||||
maxMessageBytes = srv.MaxMessageBytes
|
||||
}
|
||||
for {
|
||||
buf := make([]byte, maxMessageBytes)
|
||||
n, peerAddr, err := l.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
go func(buf []byte, peerAddr net.Addr) {
|
||||
// At least one router's UPnP implementation has added a trailing space
|
||||
// after "HTTP/1.1" - trim it.
|
||||
buf = trailingWhitespaceRx.ReplaceAllLiteral(buf, crlf)
|
||||
|
||||
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(buf)))
|
||||
if err != nil {
|
||||
log.Printf("httpu: Failed to parse request: %v", err)
|
||||
return
|
||||
}
|
||||
req.RemoteAddr = peerAddr.String()
|
||||
srv.Handler.ServeMessage(req)
|
||||
// No need to call req.Body.Close - underlying reader is bytes.Buffer.
|
||||
}(buf, peerAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve messages received on the given packet listener to the given handler.
|
||||
func Serve(l net.PacketConn, handler Handler) error {
|
||||
srv := Server{
|
||||
Handler: handler,
|
||||
MaxMessageBytes: DefaultMaxMessageBytes,
|
||||
}
|
||||
return srv.Serve(l)
|
||||
}
|
||||
4
tempfork/upnp/makefile
Normal file
4
tempfork/upnp/makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
generate:
|
||||
cd dcps/internetgateway2 && \
|
||||
go run ../../cmd/goupnpdcpgen/. -dcp_name internetgateway2
|
||||
|
||||
75
tempfork/upnp/network.go
Normal file
75
tempfork/upnp/network.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package goupnp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"tailscale.com/tempfork/upnp/httpu"
|
||||
)
|
||||
|
||||
// httpuClient creates a HTTPU client that multiplexes to all multicast-capable
|
||||
// IPv4 addresses on the host. Returns a function to clean up once the client is
|
||||
// no longer required.
|
||||
func httpuClient() (httpu.ClientInterface, func(), error) {
|
||||
addrs, err := localIPv4MCastAddrs()
|
||||
if err != nil {
|
||||
return nil, nil, ctxError(err, "requesting host IPv4 addresses")
|
||||
}
|
||||
|
||||
closers := make([]io.Closer, 0, len(addrs))
|
||||
delegates := make([]httpu.ClientInterface, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
c, err := httpu.NewHTTPUClientAddr(addr)
|
||||
if err != nil {
|
||||
return nil, nil, ctxErrorf(err,
|
||||
"creating HTTPU client for address %s", addr)
|
||||
}
|
||||
closers = append(closers, c)
|
||||
delegates = append(delegates, c)
|
||||
}
|
||||
|
||||
closer := func() {
|
||||
for _, c := range closers {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return httpu.NewMultiClient(delegates), closer, nil
|
||||
}
|
||||
|
||||
// localIPv4MCastAddrs returns the set of IPv4 addresses on multicast-able
|
||||
// network interfaces.
|
||||
func localIPv4MCastAddrs() ([]string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, ctxError(err, "requesting host interfaces")
|
||||
}
|
||||
|
||||
// Find the set of addresses to listen on.
|
||||
var addrs []string
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagMulticast == 0 || iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
||||
// Does not support multicast or is a loopback address.
|
||||
continue
|
||||
}
|
||||
ifaceAddrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return nil, ctxErrorf(err,
|
||||
"finding addresses on interface %s", iface.Name)
|
||||
}
|
||||
for _, netAddr := range ifaceAddrs {
|
||||
addr, ok := netAddr.(*net.IPNet)
|
||||
if !ok {
|
||||
// Not an IPNet address.
|
||||
continue
|
||||
}
|
||||
if addr.IP.To4() == nil {
|
||||
// Not IPv4.
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, addr.IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
167
tempfork/upnp/scpd/scpd.go
Normal file
167
tempfork/upnp/scpd/scpd.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package scpd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
SCPDXMLNamespace = "urn:schemas-upnp-org:service-1-0"
|
||||
)
|
||||
|
||||
func cleanWhitespace(s *string) {
|
||||
*s = strings.TrimSpace(*s)
|
||||
}
|
||||
|
||||
// SCPD is the service description as described by section 2.5 "Service
|
||||
// description" in
|
||||
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
||||
type SCPD struct {
|
||||
XMLName xml.Name `xml:"scpd"`
|
||||
ConfigId string `xml:"configId,attr"`
|
||||
SpecVersion SpecVersion `xml:"specVersion"`
|
||||
Actions []Action `xml:"actionList>action"`
|
||||
StateVariables []StateVariable `xml:"serviceStateTable>stateVariable"`
|
||||
}
|
||||
|
||||
// Clean attempts to remove stray whitespace etc. in the structure. It seems
|
||||
// unfortunately common for stray whitespace to be present in SCPD documents,
|
||||
// this method attempts to make it easy to clean them out.
|
||||
func (scpd *SCPD) Clean() {
|
||||
cleanWhitespace(&scpd.ConfigId)
|
||||
for i := range scpd.Actions {
|
||||
scpd.Actions[i].clean()
|
||||
}
|
||||
for i := range scpd.StateVariables {
|
||||
scpd.StateVariables[i].clean()
|
||||
}
|
||||
}
|
||||
|
||||
func (scpd *SCPD) GetStateVariable(variable string) *StateVariable {
|
||||
for i := range scpd.StateVariables {
|
||||
v := &scpd.StateVariables[i]
|
||||
if v.Name == variable {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (scpd *SCPD) GetAction(action string) *Action {
|
||||
for i := range scpd.Actions {
|
||||
a := &scpd.Actions[i]
|
||||
if a.Name == action {
|
||||
return a
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpecVersion is part of a SCPD document, describes the version of the
|
||||
// specification that the data adheres to.
|
||||
type SpecVersion struct {
|
||||
Major int32 `xml:"major"`
|
||||
Minor int32 `xml:"minor"`
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Name string `xml:"name"`
|
||||
Arguments []Argument `xml:"argumentList>argument"`
|
||||
}
|
||||
|
||||
func (action *Action) clean() {
|
||||
cleanWhitespace(&action.Name)
|
||||
for i := range action.Arguments {
|
||||
action.Arguments[i].clean()
|
||||
}
|
||||
}
|
||||
|
||||
func (action *Action) InputArguments() []*Argument {
|
||||
var result []*Argument
|
||||
for i := range action.Arguments {
|
||||
arg := &action.Arguments[i]
|
||||
if arg.IsInput() {
|
||||
result = append(result, arg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (action *Action) OutputArguments() []*Argument {
|
||||
var result []*Argument
|
||||
for i := range action.Arguments {
|
||||
arg := &action.Arguments[i]
|
||||
if arg.IsOutput() {
|
||||
result = append(result, arg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Name string `xml:"name"`
|
||||
Direction string `xml:"direction"` // in|out
|
||||
RelatedStateVariable string `xml:"relatedStateVariable"` // ?
|
||||
Retval string `xml:"retval"` // ?
|
||||
}
|
||||
|
||||
func (arg *Argument) clean() {
|
||||
cleanWhitespace(&arg.Name)
|
||||
cleanWhitespace(&arg.Direction)
|
||||
cleanWhitespace(&arg.RelatedStateVariable)
|
||||
cleanWhitespace(&arg.Retval)
|
||||
}
|
||||
|
||||
func (arg *Argument) IsInput() bool {
|
||||
return arg.Direction == "in"
|
||||
}
|
||||
|
||||
func (arg *Argument) IsOutput() bool {
|
||||
return arg.Direction == "out"
|
||||
}
|
||||
|
||||
type StateVariable struct {
|
||||
Name string `xml:"name"`
|
||||
SendEvents string `xml:"sendEvents,attr"` // yes|no
|
||||
Multicast string `xml:"multicast,attr"` // yes|no
|
||||
DataType DataType `xml:"dataType"`
|
||||
DefaultValue string `xml:"defaultValue"`
|
||||
AllowedValueRange *AllowedValueRange `xml:"allowedValueRange"`
|
||||
AllowedValues []string `xml:"allowedValueList>allowedValue"`
|
||||
}
|
||||
|
||||
func (v *StateVariable) clean() {
|
||||
cleanWhitespace(&v.Name)
|
||||
cleanWhitespace(&v.SendEvents)
|
||||
cleanWhitespace(&v.Multicast)
|
||||
v.DataType.clean()
|
||||
cleanWhitespace(&v.DefaultValue)
|
||||
if v.AllowedValueRange != nil {
|
||||
v.AllowedValueRange.clean()
|
||||
}
|
||||
for i := range v.AllowedValues {
|
||||
cleanWhitespace(&v.AllowedValues[i])
|
||||
}
|
||||
}
|
||||
|
||||
type AllowedValueRange struct {
|
||||
Minimum string `xml:"minimum"`
|
||||
Maximum string `xml:"maximum"`
|
||||
Step string `xml:"step"`
|
||||
}
|
||||
|
||||
func (r *AllowedValueRange) clean() {
|
||||
cleanWhitespace(&r.Minimum)
|
||||
cleanWhitespace(&r.Maximum)
|
||||
cleanWhitespace(&r.Step)
|
||||
}
|
||||
|
||||
type DataType struct {
|
||||
Name string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
|
||||
func (dt *DataType) clean() {
|
||||
cleanWhitespace(&dt.Name)
|
||||
cleanWhitespace(&dt.Type)
|
||||
}
|
||||
88
tempfork/upnp/service_client.go
Normal file
88
tempfork/upnp/service_client.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package goupnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
// ServiceClient is a SOAP client, root device and the service for the SOAP
|
||||
// client rolled into one value. The root device, location, and service are
|
||||
// intended to be informational. Location can be used to later recreate a
|
||||
// ServiceClient with NewServiceClientByURL if the service is still present;
|
||||
// bypassing the discovery process.
|
||||
type ServiceClient struct {
|
||||
SOAPClient *soap.SOAPClient
|
||||
RootDevice *RootDevice
|
||||
Location *url.URL
|
||||
Service *Service
|
||||
}
|
||||
|
||||
// NewServiceClients discovers services, and returns clients for them. err will
|
||||
// report any error with the discovery process (blocking any device/service
|
||||
// discovery), errors reports errors on a per-root-device basis.
|
||||
func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []error, err error) {
|
||||
var maybeRootDevices []MaybeRootDevice
|
||||
if maybeRootDevices, err = DiscoverDevices(searchTarget); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
clients = make([]ServiceClient, 0, len(maybeRootDevices))
|
||||
|
||||
for _, maybeRootDevice := range maybeRootDevices {
|
||||
if maybeRootDevice.Err != nil {
|
||||
errors = append(errors, maybeRootDevice.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
deviceClients, err := NewServiceClientsFromRootDevice(maybeRootDevice.Root, maybeRootDevice.Location, searchTarget)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
clients = append(clients, deviceClients...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewServiceClientsByURL creates client(s) for the given service URN, for a
|
||||
// root device at the given URL.
|
||||
func NewServiceClientsByURL(loc *url.URL, searchTarget string) ([]ServiceClient, error) {
|
||||
rootDevice, err := DeviceByURL(loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewServiceClientsFromRootDevice(rootDevice, loc, searchTarget)
|
||||
}
|
||||
|
||||
// NewServiceClientsFromDevice creates client(s) for the given service URN, in
|
||||
// a given root device. The loc parameter is simply assigned to the
|
||||
// Location attribute of the returned ServiceClient(s).
|
||||
func NewServiceClientsFromRootDevice(rootDevice *RootDevice, loc *url.URL, searchTarget string) ([]ServiceClient, error) {
|
||||
device := &rootDevice.Device
|
||||
srvs := device.FindService(searchTarget)
|
||||
if len(srvs) == 0 {
|
||||
return nil, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
|
||||
searchTarget, device.FriendlyName, device.UDN)
|
||||
}
|
||||
|
||||
clients := make([]ServiceClient, 0, len(srvs))
|
||||
for _, srv := range srvs {
|
||||
clients = append(clients, ServiceClient{
|
||||
SOAPClient: srv.NewSOAPClient(),
|
||||
RootDevice: rootDevice,
|
||||
Location: loc,
|
||||
Service: srv,
|
||||
})
|
||||
}
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
// GetServiceClient returns the ServiceClient itself. This is provided so that the
|
||||
// service client attributes can be accessed via an interface method on a
|
||||
// wrapping type.
|
||||
func (client *ServiceClient) GetServiceClient() *ServiceClient {
|
||||
return client
|
||||
}
|
||||
200
tempfork/upnp/soap/soap.go
Normal file
200
tempfork/upnp/soap/soap.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Definition for the SOAP structure required for UPnP's SOAP usage.
|
||||
|
||||
package soap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
|
||||
soapPrefix = xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
|
||||
soapSuffix = `</s:Body></s:Envelope>`
|
||||
)
|
||||
|
||||
type SOAPClient struct {
|
||||
EndpointURL url.URL
|
||||
HTTPClient http.Client
|
||||
}
|
||||
|
||||
func NewSOAPClient(endpointURL url.URL) *SOAPClient {
|
||||
return &SOAPClient{
|
||||
EndpointURL: endpointURL,
|
||||
}
|
||||
}
|
||||
|
||||
// PerformSOAPAction makes a SOAP request, with the given action.
|
||||
// inAction and outAction must both be pointers to structs with string fields
|
||||
// only.
|
||||
func (client *SOAPClient) PerformAction(ctx context.Context, actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
|
||||
requestBytes, err := encodeRequestAction(actionNamespace, actionName, inAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &client.EndpointURL,
|
||||
Header: http.Header{
|
||||
"SOAPACTION": []string{`"` + actionNamespace + "#" + actionName + `"`},
|
||||
"CONTENT-TYPE": []string{"text/xml; charset=\"utf-8\""},
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer(requestBytes)),
|
||||
// Set ContentLength to avoid chunked encoding - some servers might not support it.
|
||||
ContentLength: int64(len(requestBytes)),
|
||||
}
|
||||
|
||||
response, err := client.HTTPClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("goupnp: error performing SOAP HTTP request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != 200 && response.ContentLength == 0 {
|
||||
return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
|
||||
}
|
||||
|
||||
responseEnv := newSOAPEnvelope()
|
||||
decoder := xml.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(responseEnv); err != nil {
|
||||
return fmt.Errorf("goupnp: error decoding response body: %v", err)
|
||||
}
|
||||
|
||||
if responseEnv.Body.Fault != nil {
|
||||
return responseEnv.Body.Fault
|
||||
} else if response.StatusCode != 200 {
|
||||
return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
|
||||
}
|
||||
|
||||
if outAction != nil {
|
||||
if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil {
|
||||
return fmt.Errorf("goupnp: error unmarshalling out action: %v, %v", err, responseEnv.Body.RawAction)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSOAPAction creates a soapEnvelope with the given action and arguments.
|
||||
func newSOAPEnvelope() *soapEnvelope {
|
||||
return &soapEnvelope{
|
||||
EncodingStyle: soapEncodingStyle,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeRequestAction is a hacky way to create an encoded SOAP envelope
|
||||
// containing the given action. Experiments with one router have shown that it
|
||||
// 500s for requests where the outer default xmlns is set to the SOAP
|
||||
// namespace, and then reassigning the default namespace within that to the
|
||||
// service namespace. Hand-coding the outer XML to work-around this.
|
||||
func encodeRequestAction(actionNamespace, actionName string, inAction interface{}) ([]byte, error) {
|
||||
requestBuf := new(bytes.Buffer)
|
||||
requestBuf.WriteString(soapPrefix)
|
||||
requestBuf.WriteString(`<u:`)
|
||||
xml.EscapeText(requestBuf, []byte(actionName))
|
||||
requestBuf.WriteString(` xmlns:u="`)
|
||||
xml.EscapeText(requestBuf, []byte(actionNamespace))
|
||||
requestBuf.WriteString(`">`)
|
||||
if inAction != nil {
|
||||
if err := encodeRequestArgs(requestBuf, inAction); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
requestBuf.WriteString(`</u:`)
|
||||
xml.EscapeText(requestBuf, []byte(actionName))
|
||||
requestBuf.WriteString(`>`)
|
||||
requestBuf.WriteString(soapSuffix)
|
||||
return requestBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
func encodeRequestArgs(w *bytes.Buffer, inAction interface{}) error {
|
||||
in := reflect.Indirect(reflect.ValueOf(inAction))
|
||||
if in.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("goupnp: SOAP inAction is not a struct but of type %v", in.Type())
|
||||
}
|
||||
enc := xml.NewEncoder(w)
|
||||
nFields := in.NumField()
|
||||
inType := in.Type()
|
||||
for i := 0; i < nFields; i++ {
|
||||
field := inType.Field(i)
|
||||
argName := field.Name
|
||||
if nameOverride := field.Tag.Get("soap"); nameOverride != "" {
|
||||
argName = nameOverride
|
||||
}
|
||||
value := in.Field(i)
|
||||
if value.Kind() != reflect.String {
|
||||
return fmt.Errorf("goupnp: SOAP arg %q is not of type string, but of type %v", argName, value.Type())
|
||||
}
|
||||
elem := xml.StartElement{Name: xml.Name{Space: "", Local: argName}, Attr: nil}
|
||||
if err := enc.EncodeToken(elem); err != nil {
|
||||
return fmt.Errorf("goupnp: error encoding start element for SOAP arg %q: %v", argName, err)
|
||||
}
|
||||
if err := enc.Flush(); err != nil {
|
||||
return fmt.Errorf("goupnp: error flushing start element for SOAP arg %q: %v", argName, err)
|
||||
}
|
||||
if _, err := w.Write([]byte(escapeXMLText(value.Interface().(string)))); err != nil {
|
||||
return fmt.Errorf("goupnp: error writing value for SOAP arg %q: %v", argName, err)
|
||||
}
|
||||
if err := enc.EncodeToken(elem.End()); err != nil {
|
||||
return fmt.Errorf("goupnp: error encoding end element for SOAP arg %q: %v", argName, err)
|
||||
}
|
||||
}
|
||||
enc.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
var xmlCharRx = regexp.MustCompile("[<>&]")
|
||||
|
||||
// escapeXMLText is used by generated code to escape text in XML, but only
|
||||
// escaping the characters `<`, `>`, and `&`.
|
||||
//
|
||||
// This is provided in order to work around SOAP server implementations that
|
||||
// fail to decode XML correctly, specifically failing to decode `"`, `'`. Note
|
||||
// that this can only be safely used for injecting into XML text, but not into
|
||||
// attributes or other contexts.
|
||||
func escapeXMLText(s string) string {
|
||||
return xmlCharRx.ReplaceAllStringFunc(s, replaceEntity)
|
||||
}
|
||||
|
||||
func replaceEntity(s string) string {
|
||||
switch s {
|
||||
case "<":
|
||||
return "<"
|
||||
case ">":
|
||||
return ">"
|
||||
case "&":
|
||||
return "&"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type soapEnvelope struct {
|
||||
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
|
||||
EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"`
|
||||
Body soapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
|
||||
}
|
||||
|
||||
type soapBody struct {
|
||||
Fault *SOAPFaultError `xml:"Fault"`
|
||||
RawAction []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// SOAPFaultError implements error, and contains SOAP fault information.
|
||||
type SOAPFaultError struct {
|
||||
FaultCode string `xml:"faultCode"`
|
||||
FaultString string `xml:"faultString"`
|
||||
Detail struct {
|
||||
Raw []byte `xml:",innerxml"`
|
||||
} `xml:"detail"`
|
||||
}
|
||||
|
||||
func (err *SOAPFaultError) Error() string {
|
||||
return fmt.Sprintf("SOAP fault: %s", err.FaultString)
|
||||
}
|
||||
111
tempfork/upnp/soap/soap_test.go
Normal file
111
tempfork/upnp/soap/soap_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package soap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type capturingRoundTripper struct {
|
||||
err error
|
||||
resp *http.Response
|
||||
capturedReq *http.Request
|
||||
}
|
||||
|
||||
func (rt *capturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt.capturedReq = req
|
||||
return rt.resp, rt.err
|
||||
}
|
||||
|
||||
func TestActionInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
url, err := url.Parse("http://example.com/soap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rt := &capturingRoundTripper{
|
||||
err: nil,
|
||||
resp: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(`
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:myactionResponse xmlns:u="mynamespace">
|
||||
<A>valueA</A>
|
||||
<B>valueB</B>
|
||||
</u:myactionResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`)),
|
||||
},
|
||||
}
|
||||
client := SOAPClient{
|
||||
EndpointURL: *url,
|
||||
HTTPClient: http.Client{
|
||||
Transport: rt,
|
||||
},
|
||||
}
|
||||
|
||||
type In struct {
|
||||
Foo string
|
||||
Bar string `soap:"bar"`
|
||||
Baz string
|
||||
}
|
||||
type Out struct {
|
||||
A string
|
||||
B string
|
||||
}
|
||||
in := In{"foo", "bar", "quoted=\"baz\""}
|
||||
gotOut := Out{}
|
||||
err = client.PerformAction(context.Background(), "mynamespace", "myaction", &in, &gotOut)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantBody := (soapPrefix +
|
||||
`<u:myaction xmlns:u="mynamespace">` +
|
||||
`<Foo>foo</Foo>` +
|
||||
`<bar>bar</bar>` +
|
||||
`<Baz>quoted="baz"</Baz>` +
|
||||
`</u:myaction>` +
|
||||
soapSuffix)
|
||||
body, err := ioutil.ReadAll(rt.capturedReq.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotBody := string(body)
|
||||
if wantBody != gotBody {
|
||||
t.Errorf("Bad request body\nwant: %q\n got: %q", wantBody, gotBody)
|
||||
}
|
||||
|
||||
wantOut := Out{"valueA", "valueB"}
|
||||
if !reflect.DeepEqual(wantOut, gotOut) {
|
||||
t.Errorf("Bad output\nwant: %+v\n got: %+v", wantOut, gotOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeXMLText(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"abc123", "abc123"},
|
||||
{"<foo>&", "<foo>&"},
|
||||
{"\"foo'", "\"foo'"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
got := escapeXMLText(test.input)
|
||||
if got != test.want {
|
||||
t.Errorf("want %q, got %q", test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
526
tempfork/upnp/soap/types.go
Normal file
526
tempfork/upnp/soap/types.go
Normal file
@@ -0,0 +1,526 @@
|
||||
package soap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// localLoc acts like time.Local for this package, but is faked out by the
|
||||
// unit tests to ensure that things stay constant (especially when running
|
||||
// this test in a place where local time is UTC which might mask bugs).
|
||||
localLoc = time.Local
|
||||
)
|
||||
|
||||
func MarshalUi1(v uint8) (string, error) {
|
||||
return strconv.FormatUint(uint64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalUi1(s string) (uint8, error) {
|
||||
v, err := strconv.ParseUint(s, 10, 8)
|
||||
return uint8(v), err
|
||||
}
|
||||
|
||||
func MarshalUi2(v uint16) (string, error) {
|
||||
return strconv.FormatUint(uint64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalUi2(s string) (uint16, error) {
|
||||
v, err := strconv.ParseUint(s, 10, 16)
|
||||
return uint16(v), err
|
||||
}
|
||||
|
||||
func MarshalUi4(v uint32) (string, error) {
|
||||
return strconv.FormatUint(uint64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalUi4(s string) (uint32, error) {
|
||||
v, err := strconv.ParseUint(s, 10, 32)
|
||||
return uint32(v), err
|
||||
}
|
||||
|
||||
func MarshalUi8(v uint64) (string, error) {
|
||||
return strconv.FormatUint(v, 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalUi8(s string) (uint64, error) {
|
||||
v, err := strconv.ParseUint(s, 10, 64)
|
||||
return uint64(v), err
|
||||
}
|
||||
|
||||
func MarshalI1(v int8) (string, error) {
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalI1(s string) (int8, error) {
|
||||
v, err := strconv.ParseInt(s, 10, 8)
|
||||
return int8(v), err
|
||||
}
|
||||
|
||||
func MarshalI2(v int16) (string, error) {
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalI2(s string) (int16, error) {
|
||||
v, err := strconv.ParseInt(s, 10, 16)
|
||||
return int16(v), err
|
||||
}
|
||||
|
||||
func MarshalI4(v int32) (string, error) {
|
||||
return strconv.FormatInt(int64(v), 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalI4(s string) (int32, error) {
|
||||
v, err := strconv.ParseInt(s, 10, 32)
|
||||
return int32(v), err
|
||||
}
|
||||
|
||||
func MarshalInt(v int64) (string, error) {
|
||||
return strconv.FormatInt(v, 10), nil
|
||||
}
|
||||
|
||||
func UnmarshalInt(s string) (int64, error) {
|
||||
return strconv.ParseInt(s, 10, 64)
|
||||
}
|
||||
|
||||
func MarshalR4(v float32) (string, error) {
|
||||
return strconv.FormatFloat(float64(v), 'G', -1, 32), nil
|
||||
}
|
||||
|
||||
func UnmarshalR4(s string) (float32, error) {
|
||||
v, err := strconv.ParseFloat(s, 32)
|
||||
return float32(v), err
|
||||
}
|
||||
|
||||
func MarshalR8(v float64) (string, error) {
|
||||
return strconv.FormatFloat(v, 'G', -1, 64), nil
|
||||
}
|
||||
|
||||
func UnmarshalR8(s string) (float64, error) {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
return float64(v), err
|
||||
}
|
||||
|
||||
// MarshalFixed14_4 marshals float64 to SOAP "fixed.14.4" type.
|
||||
func MarshalFixed14_4(v float64) (string, error) {
|
||||
if v >= 1e14 || v <= -1e14 {
|
||||
return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'f', 4, 64), nil
|
||||
}
|
||||
|
||||
// UnmarshalFixed14_4 unmarshals float64 from SOAP "fixed.14.4" type.
|
||||
func UnmarshalFixed14_4(s string) (float64, error) {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if v >= 1e14 || v <= -1e14 {
|
||||
return 0, fmt.Errorf("soap fixed14.4: value %q out of bounds", s)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// MarshalChar marshals rune to SOAP "char" type.
|
||||
func MarshalChar(v rune) (string, error) {
|
||||
if v == 0 {
|
||||
return "", errors.New("soap char: rune 0 is not allowed")
|
||||
}
|
||||
return string(v), nil
|
||||
}
|
||||
|
||||
// UnmarshalChar unmarshals rune from SOAP "char" type.
|
||||
func UnmarshalChar(s string) (rune, error) {
|
||||
if len(s) == 0 {
|
||||
return 0, errors.New("soap char: got empty string")
|
||||
}
|
||||
r, n := utf8.DecodeRune([]byte(s))
|
||||
if n != len(s) {
|
||||
return 0, fmt.Errorf("soap char: value %q is not a single rune", s)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func MarshalString(v string) (string, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func UnmarshalString(v string) (string, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func parseInt(s string, err *error) int {
|
||||
v, parseErr := strconv.ParseInt(s, 10, 64)
|
||||
if parseErr != nil {
|
||||
*err = parseErr
|
||||
}
|
||||
return int(v)
|
||||
}
|
||||
|
||||
var dateRegexps = []*regexp.Regexp{
|
||||
// yyyy[-mm[-dd]]
|
||||
regexp.MustCompile(`^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$`),
|
||||
// yyyy[mm[dd]]
|
||||
regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`),
|
||||
}
|
||||
|
||||
func parseDateParts(s string) (year, month, day int, err error) {
|
||||
var parts []string
|
||||
for _, re := range dateRegexps {
|
||||
parts = re.FindStringSubmatch(s)
|
||||
if parts != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if parts == nil {
|
||||
err = fmt.Errorf("soap date: value %q is not in a recognized ISO8601 date format", s)
|
||||
return
|
||||
}
|
||||
|
||||
year = parseInt(parts[1], &err)
|
||||
month = 1
|
||||
day = 1
|
||||
if len(parts[2]) != 0 {
|
||||
month = parseInt(parts[2], &err)
|
||||
if len(parts[3]) != 0 {
|
||||
day = parseInt(parts[3], &err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("soap date: %q: %v", s, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var timeRegexps = []*regexp.Regexp{
|
||||
// hh[:mm[:ss]]
|
||||
regexp.MustCompile(`^(\d{2})(?::(\d{2})(?::(\d{2}))?)?$`),
|
||||
// hh[mm[ss]]
|
||||
regexp.MustCompile(`^(\d{2})(?:(\d{2})(?:(\d{2}))?)?$`),
|
||||
}
|
||||
|
||||
func parseTimeParts(s string) (hour, minute, second int, err error) {
|
||||
var parts []string
|
||||
for _, re := range timeRegexps {
|
||||
parts = re.FindStringSubmatch(s)
|
||||
if parts != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if parts == nil {
|
||||
err = fmt.Errorf("soap time: value %q is not in ISO8601 time format", s)
|
||||
return
|
||||
}
|
||||
|
||||
hour = parseInt(parts[1], &err)
|
||||
if len(parts[2]) != 0 {
|
||||
minute = parseInt(parts[2], &err)
|
||||
if len(parts[3]) != 0 {
|
||||
second = parseInt(parts[3], &err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("soap time: %q: %v", s, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// (+|-)hh[[:]mm]
|
||||
var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2})(?::?(\d{2}))?$`)
|
||||
|
||||
func parseTimezone(s string) (offset int, err error) {
|
||||
if s == "Z" {
|
||||
return 0, nil
|
||||
}
|
||||
parts := timezoneRegexp.FindStringSubmatch(s)
|
||||
if parts == nil {
|
||||
err = fmt.Errorf("soap timezone: value %q is not in ISO8601 timezone format", s)
|
||||
return
|
||||
}
|
||||
|
||||
offset = parseInt(parts[2], &err) * 3600
|
||||
if len(parts[3]) != 0 {
|
||||
offset += parseInt(parts[3], &err) * 60
|
||||
}
|
||||
if parts[1] == "-" {
|
||||
offset = -offset
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("soap timezone: %q: %v", s, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var completeDateTimeZoneRegexp = regexp.MustCompile(`^([^T]+)(?:T([^-+Z]+)(.+)?)?$`)
|
||||
|
||||
// splitCompleteDateTimeZone splits date, time and timezone apart from an
|
||||
// ISO8601 string. It does not ensure that the contents of each part are
|
||||
// correct, it merely splits on certain delimiters.
|
||||
// e.g "2010-09-08T12:15:10+0700" => "2010-09-08", "12:15:10", "+0700".
|
||||
// Timezone can only be present if time is also present.
|
||||
func splitCompleteDateTimeZone(s string) (dateStr, timeStr, zoneStr string, err error) {
|
||||
parts := completeDateTimeZoneRegexp.FindStringSubmatch(s)
|
||||
if parts == nil {
|
||||
err = fmt.Errorf("soap date/time/zone: value %q is not in ISO8601 datetime format", s)
|
||||
return
|
||||
}
|
||||
dateStr = parts[1]
|
||||
timeStr = parts[2]
|
||||
zoneStr = parts[3]
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalDate marshals time.Time to SOAP "date" type. Note that this converts
|
||||
// to local time, and discards the time-of-day components.
|
||||
func MarshalDate(v time.Time) (string, error) {
|
||||
return v.In(localLoc).Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
// UnmarshalDate unmarshals time.Time from SOAP "date" type. This outputs the
|
||||
// date as midnight in the local time zone.
|
||||
func UnmarshalDate(s string) (time.Time, error) {
|
||||
year, month, day, err := parseDateParts(s)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, localLoc), nil
|
||||
}
|
||||
|
||||
// TimeOfDay is used in cases where SOAP "time" or "time.tz" is used.
|
||||
type TimeOfDay struct {
|
||||
// Duration of time since midnight.
|
||||
FromMidnight time.Duration
|
||||
|
||||
// Set to true if Offset is specified. If false, then the timezone is
|
||||
// unspecified (and by ISO8601 - implies some "local" time).
|
||||
HasOffset bool
|
||||
|
||||
// Offset is non-zero only if time.tz is used. It is otherwise ignored. If
|
||||
// non-zero, then it is regarded as a UTC offset in seconds. Note that the
|
||||
// sub-minutes is ignored by the marshal function.
|
||||
Offset int
|
||||
}
|
||||
|
||||
// MarshalTimeOfDay marshals TimeOfDay to the "time" type.
|
||||
func MarshalTimeOfDay(v TimeOfDay) (string, error) {
|
||||
d := int64(v.FromMidnight / time.Second)
|
||||
hour := d / 3600
|
||||
d = d % 3600
|
||||
minute := d / 60
|
||||
second := d % 60
|
||||
|
||||
return fmt.Sprintf("%02d:%02d:%02d", hour, minute, second), nil
|
||||
}
|
||||
|
||||
// UnmarshalTimeOfDay unmarshals TimeOfDay from the "time" type.
|
||||
func UnmarshalTimeOfDay(s string) (TimeOfDay, error) {
|
||||
t, err := UnmarshalTimeOfDayTz(s)
|
||||
if err != nil {
|
||||
return TimeOfDay{}, err
|
||||
} else if t.HasOffset {
|
||||
return TimeOfDay{}, fmt.Errorf("soap time: value %q contains unexpected timezone", s)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// MarshalTimeOfDayTz marshals TimeOfDay to the "time.tz" type.
|
||||
func MarshalTimeOfDayTz(v TimeOfDay) (string, error) {
|
||||
d := int64(v.FromMidnight / time.Second)
|
||||
hour := d / 3600
|
||||
d = d % 3600
|
||||
minute := d / 60
|
||||
second := d % 60
|
||||
|
||||
tz := ""
|
||||
if v.HasOffset {
|
||||
if v.Offset == 0 {
|
||||
tz = "Z"
|
||||
} else {
|
||||
offsetMins := v.Offset / 60
|
||||
sign := '+'
|
||||
if offsetMins < 1 {
|
||||
offsetMins = -offsetMins
|
||||
sign = '-'
|
||||
}
|
||||
tz = fmt.Sprintf("%c%02d:%02d", sign, offsetMins/60, offsetMins%60)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%02d:%02d:%02d%s", hour, minute, second, tz), nil
|
||||
}
|
||||
|
||||
// UnmarshalTimeOfDayTz unmarshals TimeOfDay from the "time.tz" type.
|
||||
func UnmarshalTimeOfDayTz(s string) (tod TimeOfDay, err error) {
|
||||
zoneIndex := strings.IndexAny(s, "Z+-")
|
||||
var timePart string
|
||||
var hasOffset bool
|
||||
var offset int
|
||||
if zoneIndex == -1 {
|
||||
hasOffset = false
|
||||
timePart = s
|
||||
} else {
|
||||
hasOffset = true
|
||||
timePart = s[:zoneIndex]
|
||||
if offset, err = parseTimezone(s[zoneIndex:]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hour, minute, second, err := parseTimeParts(timePart)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fromMidnight := time.Duration(hour*3600+minute*60+second) * time.Second
|
||||
|
||||
// ISO8601 special case - values up to 24:00:00 are allowed, so using
|
||||
// strictly greater-than for the maximum value.
|
||||
if fromMidnight > 24*time.Hour || minute >= 60 || second >= 60 {
|
||||
return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q has value(s) out of range", s)
|
||||
}
|
||||
|
||||
return TimeOfDay{
|
||||
FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second,
|
||||
HasOffset: hasOffset,
|
||||
Offset: offset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalDateTime marshals time.Time to SOAP "dateTime" type. Note that this
|
||||
// converts to local time.
|
||||
func MarshalDateTime(v time.Time) (string, error) {
|
||||
return v.In(localLoc).Format("2006-01-02T15:04:05"), nil
|
||||
}
|
||||
|
||||
// UnmarshalDateTime unmarshals time.Time from the SOAP "dateTime" type. This
|
||||
// returns a value in the local timezone.
|
||||
func UnmarshalDateTime(s string) (result time.Time, err error) {
|
||||
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(zoneStr) != 0 {
|
||||
err = fmt.Errorf("soap datetime: unexpected timezone in %q", s)
|
||||
return
|
||||
}
|
||||
|
||||
year, month, day, err := parseDateParts(dateStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var hour, minute, second int
|
||||
if len(timeStr) != 0 {
|
||||
hour, minute, second, err = parseTimeParts(timeStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, localLoc)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalDateTimeTz marshals time.Time to SOAP "dateTime.tz" type.
|
||||
func MarshalDateTimeTz(v time.Time) (string, error) {
|
||||
return v.Format("2006-01-02T15:04:05-07:00"), nil
|
||||
}
|
||||
|
||||
// UnmarshalDateTimeTz unmarshals time.Time from the SOAP "dateTime.tz" type.
|
||||
// This returns a value in the local timezone when the timezone is unspecified.
|
||||
func UnmarshalDateTimeTz(s string) (result time.Time, err error) {
|
||||
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
year, month, day, err := parseDateParts(dateStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var hour, minute, second int
|
||||
var location *time.Location = localLoc
|
||||
if len(timeStr) != 0 {
|
||||
hour, minute, second, err = parseTimeParts(timeStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(zoneStr) != 0 {
|
||||
var offset int
|
||||
offset, err = parseTimezone(zoneStr)
|
||||
if offset == 0 {
|
||||
location = time.UTC
|
||||
} else {
|
||||
location = time.FixedZone("", offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, location)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalBoolean marshals bool to SOAP "boolean" type.
|
||||
func MarshalBoolean(v bool) (string, error) {
|
||||
if v {
|
||||
return "1", nil
|
||||
}
|
||||
return "0", nil
|
||||
}
|
||||
|
||||
// UnmarshalBoolean unmarshals bool from the SOAP "boolean" type.
|
||||
func UnmarshalBoolean(s string) (bool, error) {
|
||||
switch s {
|
||||
case "0", "false", "no":
|
||||
return false, nil
|
||||
case "1", "true", "yes":
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("soap boolean: %q is not a valid boolean value", s)
|
||||
}
|
||||
|
||||
// MarshalBinBase64 marshals []byte to SOAP "bin.base64" type.
|
||||
func MarshalBinBase64(v []byte) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(v), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinBase64 unmarshals []byte from the SOAP "bin.base64" type.
|
||||
func UnmarshalBinBase64(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// MarshalBinHex marshals []byte to SOAP "bin.hex" type.
|
||||
func MarshalBinHex(v []byte) (string, error) {
|
||||
return hex.EncodeToString(v), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinHex unmarshals []byte from the SOAP "bin.hex" type.
|
||||
func UnmarshalBinHex(s string) ([]byte, error) {
|
||||
return hex.DecodeString(s)
|
||||
}
|
||||
|
||||
// MarshalURI marshals *url.URL to SOAP "uri" type.
|
||||
func MarshalURI(v *url.URL) (string, error) {
|
||||
return v.String(), nil
|
||||
}
|
||||
|
||||
// UnmarshalURI unmarshals *url.URL from the SOAP "uri" type.
|
||||
func UnmarshalURI(s string) (*url.URL, error) {
|
||||
return url.Parse(s)
|
||||
}
|
||||
497
tempfork/upnp/soap/types_test.go
Normal file
497
tempfork/upnp/soap/types_test.go
Normal file
@@ -0,0 +1,497 @@
|
||||
package soap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type convTest interface {
|
||||
Marshal() (string, error)
|
||||
Unmarshal(string) (interface{}, error)
|
||||
Equal(result interface{}) bool
|
||||
}
|
||||
|
||||
// duper is an interface that convTest values may optionally also implement to
|
||||
// generate another convTest for a value in an otherwise identical testCase.
|
||||
type duper interface {
|
||||
Dupe(tag string) []convTest
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
value convTest
|
||||
str string
|
||||
wantMarshalErr bool
|
||||
wantUnmarshalErr bool
|
||||
noMarshal bool
|
||||
noUnMarshal bool
|
||||
tag string
|
||||
}
|
||||
|
||||
type Ui1Test uint8
|
||||
|
||||
func (v Ui1Test) Marshal() (string, error) {
|
||||
return MarshalUi1(uint8(v))
|
||||
}
|
||||
func (v Ui1Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalUi1(s)
|
||||
}
|
||||
func (v Ui1Test) Equal(result interface{}) bool {
|
||||
return uint8(v) == result.(uint8)
|
||||
}
|
||||
func (v Ui1Test) Dupe(tag string) []convTest {
|
||||
if tag == "dupe" {
|
||||
return []convTest{
|
||||
Ui2Test(v),
|
||||
Ui4Test(v),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Ui2Test uint16
|
||||
|
||||
func (v Ui2Test) Marshal() (string, error) {
|
||||
return MarshalUi2(uint16(v))
|
||||
}
|
||||
func (v Ui2Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalUi2(s)
|
||||
}
|
||||
func (v Ui2Test) Equal(result interface{}) bool {
|
||||
return uint16(v) == result.(uint16)
|
||||
}
|
||||
|
||||
type Ui4Test uint32
|
||||
|
||||
func (v Ui4Test) Marshal() (string, error) {
|
||||
return MarshalUi4(uint32(v))
|
||||
}
|
||||
func (v Ui4Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalUi4(s)
|
||||
}
|
||||
func (v Ui4Test) Equal(result interface{}) bool {
|
||||
return uint32(v) == result.(uint32)
|
||||
}
|
||||
|
||||
type I1Test int8
|
||||
|
||||
func (v I1Test) Marshal() (string, error) {
|
||||
return MarshalI1(int8(v))
|
||||
}
|
||||
func (v I1Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalI1(s)
|
||||
}
|
||||
func (v I1Test) Equal(result interface{}) bool {
|
||||
return int8(v) == result.(int8)
|
||||
}
|
||||
func (v I1Test) Dupe(tag string) []convTest {
|
||||
if tag == "dupe" {
|
||||
return []convTest{
|
||||
I2Test(v),
|
||||
I4Test(v),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type I2Test int16
|
||||
|
||||
func (v I2Test) Marshal() (string, error) {
|
||||
return MarshalI2(int16(v))
|
||||
}
|
||||
func (v I2Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalI2(s)
|
||||
}
|
||||
func (v I2Test) Equal(result interface{}) bool {
|
||||
return int16(v) == result.(int16)
|
||||
}
|
||||
|
||||
type I4Test int32
|
||||
|
||||
func (v I4Test) Marshal() (string, error) {
|
||||
return MarshalI4(int32(v))
|
||||
}
|
||||
func (v I4Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalI4(s)
|
||||
}
|
||||
func (v I4Test) Equal(result interface{}) bool {
|
||||
return int32(v) == result.(int32)
|
||||
}
|
||||
|
||||
type IntTest int64
|
||||
|
||||
func (v IntTest) Marshal() (string, error) {
|
||||
return MarshalInt(int64(v))
|
||||
}
|
||||
func (v IntTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalInt(s)
|
||||
}
|
||||
func (v IntTest) Equal(result interface{}) bool {
|
||||
return int64(v) == result.(int64)
|
||||
}
|
||||
|
||||
type Fixed14_4Test float64
|
||||
|
||||
func (v Fixed14_4Test) Marshal() (string, error) {
|
||||
return MarshalFixed14_4(float64(v))
|
||||
}
|
||||
func (v Fixed14_4Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalFixed14_4(s)
|
||||
}
|
||||
func (v Fixed14_4Test) Equal(result interface{}) bool {
|
||||
return math.Abs(float64(v)-result.(float64)) < 0.001
|
||||
}
|
||||
|
||||
type CharTest rune
|
||||
|
||||
func (v CharTest) Marshal() (string, error) {
|
||||
return MarshalChar(rune(v))
|
||||
}
|
||||
func (v CharTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalChar(s)
|
||||
}
|
||||
func (v CharTest) Equal(result interface{}) bool {
|
||||
return rune(v) == result.(rune)
|
||||
}
|
||||
|
||||
type DateTest struct{ time.Time }
|
||||
|
||||
func (v DateTest) Marshal() (string, error) {
|
||||
return MarshalDate(time.Time(v.Time))
|
||||
}
|
||||
func (v DateTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalDate(s)
|
||||
}
|
||||
func (v DateTest) Equal(result interface{}) bool {
|
||||
return v.Time.Equal(result.(time.Time))
|
||||
}
|
||||
func (v DateTest) Dupe(tag string) []convTest {
|
||||
if tag != "no:dateTime" {
|
||||
return []convTest{DateTimeTest(v)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TimeOfDayTest struct {
|
||||
TimeOfDay
|
||||
}
|
||||
|
||||
func (v TimeOfDayTest) Marshal() (string, error) {
|
||||
return MarshalTimeOfDay(v.TimeOfDay)
|
||||
}
|
||||
func (v TimeOfDayTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalTimeOfDay(s)
|
||||
}
|
||||
func (v TimeOfDayTest) Equal(result interface{}) bool {
|
||||
return v.TimeOfDay == result.(TimeOfDay)
|
||||
}
|
||||
func (v TimeOfDayTest) Dupe(tag string) []convTest {
|
||||
if tag != "no:time.tz" {
|
||||
return []convTest{TimeOfDayTzTest(v)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TimeOfDayTzTest struct {
|
||||
TimeOfDay
|
||||
}
|
||||
|
||||
func (v TimeOfDayTzTest) Marshal() (string, error) {
|
||||
return MarshalTimeOfDayTz(v.TimeOfDay)
|
||||
}
|
||||
func (v TimeOfDayTzTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalTimeOfDayTz(s)
|
||||
}
|
||||
func (v TimeOfDayTzTest) Equal(result interface{}) bool {
|
||||
return v.TimeOfDay == result.(TimeOfDay)
|
||||
}
|
||||
|
||||
type DateTimeTest struct{ time.Time }
|
||||
|
||||
func (v DateTimeTest) Marshal() (string, error) {
|
||||
return MarshalDateTime(time.Time(v.Time))
|
||||
}
|
||||
func (v DateTimeTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalDateTime(s)
|
||||
}
|
||||
func (v DateTimeTest) Equal(result interface{}) bool {
|
||||
return v.Time.Equal(result.(time.Time))
|
||||
}
|
||||
func (v DateTimeTest) Dupe(tag string) []convTest {
|
||||
if tag != "no:dateTime.tz" {
|
||||
return []convTest{DateTimeTzTest(v)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DateTimeTzTest struct{ time.Time }
|
||||
|
||||
func (v DateTimeTzTest) Marshal() (string, error) {
|
||||
return MarshalDateTimeTz(time.Time(v.Time))
|
||||
}
|
||||
func (v DateTimeTzTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalDateTimeTz(s)
|
||||
}
|
||||
func (v DateTimeTzTest) Equal(result interface{}) bool {
|
||||
return v.Time.Equal(result.(time.Time))
|
||||
}
|
||||
|
||||
type BooleanTest bool
|
||||
|
||||
func (v BooleanTest) Marshal() (string, error) {
|
||||
return MarshalBoolean(bool(v))
|
||||
}
|
||||
func (v BooleanTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalBoolean(s)
|
||||
}
|
||||
func (v BooleanTest) Equal(result interface{}) bool {
|
||||
return bool(v) == result.(bool)
|
||||
}
|
||||
|
||||
type BinBase64Test []byte
|
||||
|
||||
func (v BinBase64Test) Marshal() (string, error) {
|
||||
return MarshalBinBase64([]byte(v))
|
||||
}
|
||||
func (v BinBase64Test) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalBinBase64(s)
|
||||
}
|
||||
func (v BinBase64Test) Equal(result interface{}) bool {
|
||||
return bytes.Equal([]byte(v), result.([]byte))
|
||||
}
|
||||
|
||||
type BinHexTest []byte
|
||||
|
||||
func (v BinHexTest) Marshal() (string, error) {
|
||||
return MarshalBinHex([]byte(v))
|
||||
}
|
||||
func (v BinHexTest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalBinHex(s)
|
||||
}
|
||||
func (v BinHexTest) Equal(result interface{}) bool {
|
||||
return bytes.Equal([]byte(v), result.([]byte))
|
||||
}
|
||||
|
||||
type URITest struct{ URL *url.URL }
|
||||
|
||||
func (v URITest) Marshal() (string, error) {
|
||||
return MarshalURI(v.URL)
|
||||
}
|
||||
func (v URITest) Unmarshal(s string) (interface{}, error) {
|
||||
return UnmarshalURI(s)
|
||||
}
|
||||
func (v URITest) Equal(result interface{}) bool {
|
||||
return v.URL.String() == result.(*url.URL).String()
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
const time010203 time.Duration = (1*3600 + 2*60 + 3) * time.Second
|
||||
const time0102 time.Duration = (1*3600 + 2*60) * time.Second
|
||||
const time01 time.Duration = (1 * 3600) * time.Second
|
||||
const time235959 time.Duration = (23*3600 + 59*60 + 59) * time.Second
|
||||
|
||||
// Fake out the local time for the implementation.
|
||||
localLoc = time.FixedZone("Fake/Local", 6*3600)
|
||||
defer func() {
|
||||
localLoc = time.Local
|
||||
}()
|
||||
|
||||
tests := []testCase{
|
||||
// ui1
|
||||
{str: "", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: " ", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: "abc", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: "-1", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: "0", value: Ui1Test(0), tag: "dupe"},
|
||||
{str: "1", value: Ui1Test(1), tag: "dupe"},
|
||||
{str: "255", value: Ui1Test(255), tag: "dupe"},
|
||||
{str: "256", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// ui2
|
||||
{str: "65535", value: Ui2Test(65535)},
|
||||
{str: "65536", value: Ui2Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// ui4
|
||||
{str: "4294967295", value: Ui4Test(4294967295)},
|
||||
{str: "4294967296", value: Ui4Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// i1
|
||||
{str: "", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: " ", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: "abc", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
|
||||
{str: "0", value: I1Test(0), tag: "dupe"},
|
||||
{str: "-1", value: I1Test(-1), tag: "dupe"},
|
||||
{str: "127", value: I1Test(127), tag: "dupe"},
|
||||
{str: "-128", value: I1Test(-128), tag: "dupe"},
|
||||
{str: "128", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "-129", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// i2
|
||||
{str: "32767", value: I2Test(32767)},
|
||||
{str: "-32768", value: I2Test(-32768)},
|
||||
{str: "32768", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "-32769", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// i4
|
||||
{str: "2147483647", value: I4Test(2147483647)},
|
||||
{str: "-2147483648", value: I4Test(-2147483648)},
|
||||
{str: "2147483648", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "-2147483649", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// int
|
||||
{str: "9223372036854775807", value: IntTest(9223372036854775807)},
|
||||
{str: "-9223372036854775808", value: IntTest(-9223372036854775808)},
|
||||
{str: "9223372036854775808", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "-9223372036854775809", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// fixed.14.4
|
||||
{str: "0.0000", value: Fixed14_4Test(0)},
|
||||
{str: "1.0000", value: Fixed14_4Test(1)},
|
||||
{str: "1.2346", value: Fixed14_4Test(1.23456)},
|
||||
{str: "-1.0000", value: Fixed14_4Test(-1)},
|
||||
{str: "-1.2346", value: Fixed14_4Test(-1.23456)},
|
||||
{str: "10000000000000.0000", value: Fixed14_4Test(1e13)},
|
||||
{str: "100000000000000.0000", value: Fixed14_4Test(1e14), wantMarshalErr: true, wantUnmarshalErr: true},
|
||||
{str: "-10000000000000.0000", value: Fixed14_4Test(-1e13)},
|
||||
{str: "-100000000000000.0000", value: Fixed14_4Test(-1e14), wantMarshalErr: true, wantUnmarshalErr: true},
|
||||
|
||||
// char
|
||||
{str: "a", value: CharTest('a')},
|
||||
{str: "z", value: CharTest('z')},
|
||||
{str: "\u1234", value: CharTest(0x1234)},
|
||||
{str: "aa", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
|
||||
{str: "", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
|
||||
|
||||
// date
|
||||
{str: "2013-10-08", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime"},
|
||||
{str: "20131008", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true, tag: "no:dateTime"},
|
||||
{str: "2013-10-08T10:30:50", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
|
||||
{str: "2013-10-08T10:30:50Z", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
|
||||
{str: "", value: DateTest{}, wantMarshalErr: true, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "-1", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
|
||||
// time
|
||||
{str: "00:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}},
|
||||
{str: "000000", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}, noMarshal: true},
|
||||
{str: "24:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 24 * time.Hour}}, noMarshal: true}, // ISO8601 special case
|
||||
{str: "24:01:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "24:00:01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "25:00:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "00:60:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "00:00:60", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "01:02:03", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}},
|
||||
{str: "010203", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}, noMarshal: true},
|
||||
{str: "23:59:59", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}},
|
||||
{str: "235959", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}, noMarshal: true},
|
||||
{str: "01:02", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
|
||||
{str: "0102", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
|
||||
{str: "01", value: TimeOfDayTest{TimeOfDay{FromMidnight: time01}}, noMarshal: true},
|
||||
{str: "foo 01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "foo\n01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03 foo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03\nfoo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03Z", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03+01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03+01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03+0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03-01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03-01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
{str: "01:02:03-0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
|
||||
|
||||
// time.tz
|
||||
{str: "24:00:01", value: TimeOfDayTzTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "01Z", value: TimeOfDayTzTest{TimeOfDay{time01, true, 0}}, noMarshal: true},
|
||||
{str: "01:02:03Z", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 0}}},
|
||||
{str: "01+01", value: TimeOfDayTzTest{TimeOfDay{time01, true, 3600}}, noMarshal: true},
|
||||
{str: "01:02:03+01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600}}, noMarshal: true},
|
||||
{str: "01:02:03+01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}},
|
||||
{str: "01:02:03+0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}, noMarshal: true},
|
||||
{str: "01:02:03-01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -3600}}, noMarshal: true},
|
||||
{str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}},
|
||||
{str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true},
|
||||
|
||||
// dateTime
|
||||
{str: "2013-10-08T00:00:00", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime.tz"},
|
||||
{str: "20131008", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50", value: DateTimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50T", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50+01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50+0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50-01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
{str: "2013-10-08T10:30:50-0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
|
||||
|
||||
// dateTime.tz
|
||||
{str: "2013-10-08T10:30:50", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50+01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600))}, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}},
|
||||
{str: "2013-10-08T10:30:50+0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50-01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600))}, noMarshal: true},
|
||||
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}},
|
||||
{str: "2013-10-08T10:30:50-0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}, noMarshal: true},
|
||||
|
||||
// boolean
|
||||
{str: "0", value: BooleanTest(false)},
|
||||
{str: "1", value: BooleanTest(true)},
|
||||
{str: "false", value: BooleanTest(false), noMarshal: true},
|
||||
{str: "true", value: BooleanTest(true), noMarshal: true},
|
||||
{str: "no", value: BooleanTest(false), noMarshal: true},
|
||||
{str: "yes", value: BooleanTest(true), noMarshal: true},
|
||||
{str: "", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
|
||||
{str: "other", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
|
||||
{str: "2", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
|
||||
{str: "-1", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
|
||||
|
||||
// bin.base64
|
||||
{str: "", value: BinBase64Test{}},
|
||||
{str: "YQ==", value: BinBase64Test("a")},
|
||||
{str: "TG9uZ2VyIFN0cmluZy4=", value: BinBase64Test("Longer String.")},
|
||||
{str: "TG9uZ2VyIEFsaWduZWQu", value: BinBase64Test("Longer Aligned.")},
|
||||
|
||||
// bin.hex
|
||||
{str: "", value: BinHexTest{}},
|
||||
{str: "61", value: BinHexTest("a")},
|
||||
{str: "4c6f6e67657220537472696e672e", value: BinHexTest("Longer String.")},
|
||||
{str: "4C6F6E67657220537472696E672E", value: BinHexTest("Longer String."), noMarshal: true},
|
||||
|
||||
// uri
|
||||
{str: "http://example.com/path", value: URITest{&url.URL{Scheme: "http", Host: "example.com", Path: "/path"}}},
|
||||
}
|
||||
|
||||
// Generate extra test cases from convTests that implement duper.
|
||||
var extras []testCase
|
||||
for i := range tests {
|
||||
if duper, ok := tests[i].value.(duper); ok {
|
||||
dupes := duper.Dupe(tests[i].tag)
|
||||
for _, duped := range dupes {
|
||||
dupedCase := testCase(tests[i])
|
||||
dupedCase.value = duped
|
||||
extras = append(extras, dupedCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
tests = append(tests, extras...)
|
||||
|
||||
for _, test := range tests {
|
||||
if test.noMarshal {
|
||||
} else if resultStr, err := test.value.Marshal(); err != nil && !test.wantMarshalErr {
|
||||
t.Errorf("For %T marshal %v, want %q, got error: %v", test.value, test.value, test.str, err)
|
||||
} else if err == nil && test.wantMarshalErr {
|
||||
t.Errorf("For %T marshal %v, want error, got %q", test.value, test.value, resultStr)
|
||||
} else if err == nil && resultStr != test.str {
|
||||
t.Errorf("For %T marshal %v, want %q, got %q", test.value, test.value, test.str, resultStr)
|
||||
}
|
||||
|
||||
if test.noUnMarshal {
|
||||
} else if resultValue, err := test.value.Unmarshal(test.str); err != nil && !test.wantUnmarshalErr {
|
||||
t.Errorf("For %T unmarshal %q, want %v, got error: %v", test.value, test.str, test.value, err)
|
||||
} else if err == nil && test.wantUnmarshalErr {
|
||||
t.Errorf("For %T unmarshal %q, want error, got %v", test.value, test.str, resultValue)
|
||||
} else if err == nil && !test.value.Equal(resultValue) {
|
||||
t.Errorf("For %T unmarshal %q, want %v, got %v", test.value, test.str, test.value, resultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
tempfork/upnp/ssdp/ssdp.go
Normal file
102
tempfork/upnp/ssdp/ssdp.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ssdpDiscover = `"ssdp:discover"`
|
||||
ntsAlive = `ssdp:alive`
|
||||
ntsByebye = `ssdp:byebye`
|
||||
ntsUpdate = `ssdp:update`
|
||||
ssdpUDP4Addr = "239.255.255.250:1900"
|
||||
ssdpSearchPort = 1900
|
||||
methodSearch = "M-SEARCH"
|
||||
methodNotify = "NOTIFY"
|
||||
|
||||
// SSDPAll is a value for searchTarget that searches for all devices and services.
|
||||
SSDPAll = "ssdp:all"
|
||||
// UPNPRootDevice is a value for searchTarget that searches for all root devices.
|
||||
UPNPRootDevice = "upnp:rootdevice"
|
||||
)
|
||||
|
||||
// HTTPUClient is the interface required to perform HTTP-over-UDP requests.
|
||||
type HTTPUClient interface {
|
||||
Do(
|
||||
req *http.Request,
|
||||
timeout time.Duration,
|
||||
numSends int,
|
||||
) ([]*http.Response, error)
|
||||
}
|
||||
|
||||
// SSDPRawSearch performs a fairly raw SSDP search request, and returns the
|
||||
// unique response(s) that it receives. Each response has the requested
|
||||
// searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
|
||||
// wait for responses in seconds, and must be a minimum of 1 (the
|
||||
// implementation waits an additional 100ms for responses to arrive), 2 is a
|
||||
// reasonable value for this. numSends is the number of requests to send - 3 is
|
||||
// a reasonable value for this.
|
||||
func SSDPRawSearch(
|
||||
httpu HTTPUClient,
|
||||
searchTarget string,
|
||||
maxWaitSeconds int,
|
||||
numSends int,
|
||||
) ([]*http.Response, error) {
|
||||
if maxWaitSeconds < 1 {
|
||||
return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
|
||||
}
|
||||
|
||||
req := http.Request{
|
||||
Method: methodSearch,
|
||||
// TODO: Support both IPv4 and IPv6.
|
||||
Host: ssdpUDP4Addr,
|
||||
URL: &url.URL{Opaque: "*"},
|
||||
Header: http.Header{
|
||||
// Putting headers in here avoids them being title-cased.
|
||||
// (The UPnP discovery protocol uses case-sensitive headers)
|
||||
"HOST": []string{ssdpUDP4Addr},
|
||||
"MX": []string{strconv.FormatInt(int64(maxWaitSeconds), 10)},
|
||||
"MAN": []string{ssdpDiscover},
|
||||
"ST": []string{searchTarget},
|
||||
},
|
||||
}
|
||||
allResponses, err := httpu.Do(&req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
|
||||
|
||||
seenUSNs := make(map[string]bool)
|
||||
var responses []*http.Response
|
||||
for _, response := range allResponses {
|
||||
if response.StatusCode != 200 {
|
||||
log.Printf("ssdp: got response status code %q in search response", response.Status)
|
||||
continue
|
||||
}
|
||||
if st := response.Header.Get("ST"); isExactSearch && st != searchTarget {
|
||||
continue
|
||||
}
|
||||
usn := response.Header.Get("USN")
|
||||
if usn == "" {
|
||||
// Empty/missing USN in search response - using location instead.
|
||||
location, err := response.Location()
|
||||
if err != nil {
|
||||
// No usable location in search response - discard.
|
||||
continue
|
||||
}
|
||||
usn = location.String()
|
||||
}
|
||||
if _, alreadySeen := seenUSNs[usn]; !alreadySeen {
|
||||
seenUSNs[usn] = true
|
||||
responses = append(responses, response)
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
Reference in New Issue
Block a user