Discover devices on all the host's capable network interfaces.
This commit is contained in:
parent
0c863b7f0d
commit
36abb0b21b
1
go.mod
1
go.mod
@ -5,5 +5,6 @@ go 1.14
|
|||||||
require (
|
require (
|
||||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150
|
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150
|
||||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1
|
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||||
golang.org/x/text v0.3.0 // indirect
|
golang.org/x/text v0.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
2
go.sum
2
go.sum
@ -2,5 +2,7 @@ github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 h1:vlNjIqmUZ9CMAWsbURY
|
|||||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w=
|
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w=
|
||||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
24
goupnp.go
24
goupnp.go
@ -21,10 +21,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/html/charset"
|
|
||||||
|
|
||||||
"github.com/huin/goupnp/httpu"
|
|
||||||
"github.com/huin/goupnp/ssdp"
|
"github.com/huin/goupnp/ssdp"
|
||||||
|
"golang.org/x/net/html/charset"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContextError is an error that wraps an error with some context information.
|
// ContextError is an error that wraps an error with some context information.
|
||||||
@ -33,6 +31,20 @@ type ContextError struct {
|
|||||||
Err error
|
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 {
|
func (err ContextError) Error() string {
|
||||||
return fmt.Sprintf("%s: %v", err.Context, err.Err)
|
return fmt.Sprintf("%s: %v", err.Context, err.Err)
|
||||||
}
|
}
|
||||||
@ -61,12 +73,12 @@ type MaybeRootDevice struct {
|
|||||||
// while attempting to send the query. An error or RootDevice is returned for
|
// while attempting to send the query. An error or RootDevice is returned for
|
||||||
// each discovered RootDevice.
|
// each discovered RootDevice.
|
||||||
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
|
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
|
||||||
httpu, err := httpu.NewHTTPUClient()
|
hc, hcCleanup, err := httpuClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer httpu.Close()
|
defer hcCleanup()
|
||||||
responses, err := ssdp.SSDPRawSearch(httpu, string(searchTarget), 2, 3)
|
responses, err := ssdp.SSDPRawSearch(hc, string(searchTarget), 2, 3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,20 @@ import (
|
|||||||
"time"
|
"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
|
// HTTPUClient is a client for dealing with HTTPU (HTTP over UDP). Its typical
|
||||||
// function is for HTTPMU, and particularly SSDP.
|
// function is for HTTPMU, and particularly SSDP.
|
||||||
type HTTPUClient struct {
|
type HTTPUClient struct {
|
||||||
@ -19,6 +33,8 @@ type HTTPUClient struct {
|
|||||||
conn net.PacketConn
|
conn net.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ ClientInterface = &HTTPUClient{}
|
||||||
|
|
||||||
// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
|
// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
|
||||||
// purpose.
|
// purpose.
|
||||||
func NewHTTPUClient() (*HTTPUClient, error) {
|
func NewHTTPUClient() (*HTTPUClient, error) {
|
||||||
@ -51,14 +67,15 @@ func (httpu *HTTPUClient) Close() error {
|
|||||||
return httpu.conn.Close()
|
return httpu.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do performs a request. The timeout is how long to wait for before returning
|
// Do implements ClientInterface.Do.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Note that at present only one concurrent connection will happen per
|
// Note that at present only one concurrent connection will happen per
|
||||||
// HTTPUClient.
|
// HTTPUClient.
|
||||||
func (httpu *HTTPUClient) Do(req *http.Request, timeout time.Duration, numSends int) ([]*http.Response, error) {
|
func (httpu *HTTPUClient) Do(
|
||||||
|
req *http.Request,
|
||||||
|
timeout time.Duration,
|
||||||
|
numSends int,
|
||||||
|
) ([]*http.Response, error) {
|
||||||
httpu.connLock.Lock()
|
httpu.connLock.Lock()
|
||||||
defer httpu.connLock.Unlock()
|
defer httpu.connLock.Unlock()
|
||||||
|
|
||||||
|
70
httpu/multiclient.go
Normal file
70
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()
|
||||||
|
}
|
75
network.go
Normal file
75
network.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package goupnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/huin/goupnp/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
|
||||||
|
}
|
||||||
|
|
||||||
|
// localIPv2MCastAddrs 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 {
|
||||||
|
// Does not support multicast.
|
||||||
|
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
|
||||||
|
}
|
38
ssdp/ssdp.go
38
ssdp/ssdp.go
@ -7,8 +7,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/huin/goupnp/httpu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -27,6 +25,15 @@ const (
|
|||||||
UPNPRootDevice = "upnp:rootdevice"
|
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
|
// SSDPRawSearch performs a fairly raw SSDP search request, and returns the
|
||||||
// unique response(s) that it receives. Each response has the requested
|
// unique response(s) that it receives. Each response has the requested
|
||||||
// searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
|
// searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
|
||||||
@ -34,13 +41,16 @@ const (
|
|||||||
// implementation waits an additional 100ms for responses to arrive), 2 is a
|
// 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
|
// reasonable value for this. numSends is the number of requests to send - 3 is
|
||||||
// a reasonable value for this.
|
// a reasonable value for this.
|
||||||
func SSDPRawSearch(httpu *httpu.HTTPUClient, searchTarget string, maxWaitSeconds int, numSends int) ([]*http.Response, error) {
|
func SSDPRawSearch(
|
||||||
|
httpu HTTPUClient,
|
||||||
|
searchTarget string,
|
||||||
|
maxWaitSeconds int,
|
||||||
|
numSends int,
|
||||||
|
) ([]*http.Response, error) {
|
||||||
if maxWaitSeconds < 1 {
|
if maxWaitSeconds < 1 {
|
||||||
return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
|
return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
seenUsns := make(map[string]bool)
|
|
||||||
var responses []*http.Response
|
|
||||||
req := http.Request{
|
req := http.Request{
|
||||||
Method: methodSearch,
|
Method: methodSearch,
|
||||||
// TODO: Support both IPv4 and IPv6.
|
// TODO: Support both IPv4 and IPv6.
|
||||||
@ -62,6 +72,8 @@ func SSDPRawSearch(httpu *httpu.HTTPUClient, searchTarget string, maxWaitSeconds
|
|||||||
|
|
||||||
isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
|
isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
|
||||||
|
|
||||||
|
seenUSNs := make(map[string]bool)
|
||||||
|
var responses []*http.Response
|
||||||
for _, response := range allResponses {
|
for _, response := range allResponses {
|
||||||
if response.StatusCode != 200 {
|
if response.StatusCode != 200 {
|
||||||
log.Printf("ssdp: got response status code %q in search response", response.Status)
|
log.Printf("ssdp: got response status code %q in search response", response.Status)
|
||||||
@ -70,18 +82,18 @@ func SSDPRawSearch(httpu *httpu.HTTPUClient, searchTarget string, maxWaitSeconds
|
|||||||
if st := response.Header.Get("ST"); isExactSearch && st != searchTarget {
|
if st := response.Header.Get("ST"); isExactSearch && st != searchTarget {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
location, err := response.Location()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("ssdp: no usable location in search response (discarding): %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
usn := response.Header.Get("USN")
|
usn := response.Header.Get("USN")
|
||||||
if usn == "" {
|
if usn == "" {
|
||||||
log.Printf("ssdp: empty/missing USN in search response (using location instead): %v", err)
|
// 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()
|
usn = location.String()
|
||||||
}
|
}
|
||||||
if _, alreadySeen := seenUsns[usn]; !alreadySeen {
|
if _, alreadySeen := seenUSNs[usn]; !alreadySeen {
|
||||||
seenUsns[usn] = true
|
seenUSNs[usn] = true
|
||||||
responses = append(responses, response)
|
responses = append(responses, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user