tailscale/net/upnp/service_client.go
julianknodt c6b92ddda8 Copy goupnp client into our repo
goupnp is an existing upnp client for go, which provides all the functionality we need, licensed
under BSD-2-Clause, so we can copy it over and modify parts of it for our case.
Specifically, we add contexts to all the methods so we can better handle timeouts, remove the
dependency on large charsets, and (eventually) trim out extra components we don't need.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-14 13:40:02 -07:00

89 lines
2.8 KiB
Go

package goupnp
import (
"fmt"
"net/url"
"tailscale.com/net/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
}