Integrate BACKBEAT SDK and resolve KACHING license validation

Major integrations and fixes:
- Added BACKBEAT SDK integration for P2P operation timing
- Implemented beat-aware status tracking for distributed operations
- Added Docker secrets support for secure license management
- Resolved KACHING license validation via HTTPS/TLS
- Updated docker-compose configuration for clean stack deployment
- Disabled rollback policies to prevent deployment failures
- Added license credential storage (CHORUS-DEV-MULTI-001)

Technical improvements:
- BACKBEAT P2P operation tracking with phase management
- Enhanced configuration system with file-based secrets
- Improved error handling for license validation
- Clean separation of KACHING and CHORUS deployment stacks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-06 07:56:26 +10:00
parent 543ab216f9
commit 9bdcbe0447
4730 changed files with 1480093 additions and 1916 deletions

3
vendor/github.com/huin/goupnp/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
*.zip
*.sublime-workspace
*.download

133
vendor/github.com/huin/goupnp/GUIDE.md generated vendored Normal file
View File

@@ -0,0 +1,133 @@
# Guide
This is a quick guide with example code for common use cases that might be
helpful for people wanting a quick guide to common ways that people would
want to use this library.
## Internet Gateways
`goupnp/dcps/internetgateway1` and `goupnp/dcps/internetgateway2` implement
different version standards that allow clients to interact with devices like
home consumer routers, but you can probably get by with just
`internetgateway2`. Some very common use cases to talk to such devices are:
- Requesting the external Internet-facing IP address.
- Requesting a port be forwarded from the external (Internet-facing) interface
to a port on the LAN.
Different routers implement different standards, so you may have to request
multiple clients to find the one that your router needs. The most useful ones
for the purpose above can be requested with the following functions:
- `internetgateway2.NewWANIPConnection1Clients()`
- `internetgateway2.NewWANIPConnection2Clients()`
- `internetgateway2.NewWANPPPConnection1Clients()`
Fortunately, each of the clients returned by these functions provide the same
method signatures for the purposes listed above. So you could request multiple
clients, and whichever one you find, and return it from a function in a variable
of the common interface, e.g:
```go
type RouterClient interface {
AddPortMapping(
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error)
GetExternalIPAddress() (
NewExternalIPAddress string,
err error,
)
}
func PickRouterClient(ctx context.Context) (RouterClient, error) {
tasks, _ := errgroup.WithContext(ctx)
// Request each type of client in parallel, and return what is found.
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
})
if err := tasks.Wait(); err != nil {
return nil, err
}
// Trivial handling for where we find exactly one device to talk to, you
// might want to provide more flexible handling than this if multiple
// devices are found.
switch {
case len(ip2Clients) == 1:
return ip2Clients[0], nil
case len(ip1Clients) == 1:
return ip1Clients[0], nil
case len(ppp1Clients) == 1:
return ppp1Clients[0], nil
default:
return nil, errors.New("multiple or no services found")
}
}
```
You could then use this function to create a client, and both request the
external IP address and forward it to a port on your local network, e.g:
```go
func GetIPAndForwardPort(ctx context.Context) error {
client, err := PickRouterClient(ctx)
if err != nil {
return err
}
externalIP, err := client.GetExternalIPAddress()
if err != nil {
return err
}
fmt.Println("Our external IP address is: ", externalIP)
return client.AddPortMapping(
"",
// External port number to expose to Internet:
1234,
// Forward TCP (this could be "UDP" if we wanted that instead).
"TCP",
// Internal port number on the LAN to forward to.
// Some routers might not support this being different to the external
// port number.
1234,
// Internal address on the LAN we want to forward to.
"192.168.1.6",
// Enabled:
true,
// Informational description for the client requesting the port forwarding.
"MyProgramName",
// How long should the port forward last for in seconds.
// If you want to keep it open for longer and potentially across router
// resets, you might want to periodically request before this elapses.
3600,
)
}
```
The code above is of course just a relatively trivial example that you can
tailor to your own use case.

23
vendor/github.com/huin/goupnp/LICENSE generated vendored Normal file
View 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.

2
vendor/github.com/huin/goupnp/Makefile generated vendored Normal file
View File

@@ -0,0 +1,2 @@
gen:
(cd cmd/goupnpdcpgen/; go install); go generate ./...

76
vendor/github.com/huin/goupnp/README.md generated vendored Normal file
View File

@@ -0,0 +1,76 @@
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):
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) av1](https://godoc.org/github.com/huin/goupnp/dcps/av1) - Client for UPnP Device Control Protocol MediaServer v1 and MediaRenderer v1.
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) internetgateway1](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway1) - Client for UPnP Device Control Protocol Internet Gateway Device v1.
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) internetgateway2](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway2) - Client for UPnP Device Control Protocol Internet Gateway Device v2.
Core components:
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) (goupnp)](https://godoc.org/github.com/huin/goupnp) core library - contains datastructures and utilities typically used by the implemented DCPs.
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) httpu](https://godoc.org/github.com/huin/goupnp/httpu) HTTPU implementation, underlies SSDP.
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) ssdp](https://godoc.org/github.com/huin/goupnp/ssdp) SSDP client implementation (simple service discovery protocol) - used to discover UPnP services on a network.
- [![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg) 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.
## Migrating due to Breaking Changes
- \#40 introduced a breaking change to handling non-utf8 encodings, but removes a heavy
dependency on `golang.org/x/net` with charset encodings. If this breaks your usage of this
library, you can return to the old behavior by modifying the exported variable and importing
the package yourself:
```go
import (
"golang.org/x/net/html/charset"
"github.com/huin/goupnp"
)
func init() {
// should be modified before goupnp libraries are in use.
goupnp.CharsetReaderFault = charset.NewReaderLabel
}
```
## `v2alpha`
The `v2alpha` subdirectory contains experimental work on a version 2 API. The plan is to eventually
create a `v2` subdirectory with a stable version of the version 2 API. The v1 API will stay where
it currently is.
> NOTE:
>
> * `v2alpha` will be deleted one day, so don't rely on it always existing.
> * `v2alpha` will have API breaking changes, even with itself.

View File

@@ -0,0 +1,2 @@
//go:generate goupnpdcpgen -dcp_name internetgateway1 -code_tmpl_file ../dcps.gotemplate
package internetgateway1

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
//go:generate goupnpdcpgen -dcp_name internetgateway2 -code_tmpl_file ../dcps.gotemplate
package internetgateway2

File diff suppressed because it is too large Load Diff

204
vendor/github.com/huin/goupnp/device.go generated vendored Normal file
View File

@@ -0,0 +1,204 @@
// This file contains XML structures for communicating with UPnP devices.
package goupnp
import (
"context"
"encoding/xml"
"errors"
"fmt"
"net/url"
"strings"
"github.com/huin/goupnp/scpd"
"github.com/huin/goupnp/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"`
ModelType string `xml:"modelType"`
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)
}
// RequestSCPDCtx requests the SCPD (soap actions and state variables description)
// for the service.
func (srv *Service) RequestSCPDCtx(ctx context.Context) (*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(ctx, srv.SCPDURL.URL.String(), scpd.SCPDXMLNamespace, s); err != nil {
return nil, err
}
return s, nil
}
// RequestSCPD is the legacy version of RequestSCPDCtx, but uses
// context.Background() as the context.
func (srv *Service) RequestSCPD() (*scpd.SCPD, error) {
return srv.RequestSCPDCtx(context.Background())
}
// 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) {
str := uf.Str
if !strings.Contains(str, "://") && !strings.HasPrefix(str, "/") {
str = "/" + str
}
refUrl, err := url.Parse(str)
if err != nil {
uf.URL = url.URL{}
uf.Ok = false
return
}
uf.URL = *urlBase.ResolveReference(refUrl)
uf.Ok = true
}

183
vendor/github.com/huin/goupnp/goupnp.go generated vendored Normal file
View File

@@ -0,0 +1,183 @@
// 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 (
"context"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/huin/goupnp/httpu"
"github.com/huin/goupnp/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. Note that this in combination with Location
// uniquely identifies a result from DiscoverDevices.
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
// The address from which the device was discovered (if known - otherwise nil).
LocalAddr net.IP
// Any error encountered probing a discovered device.
Err error
}
// DiscoverDevicesCtx 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 DiscoverDevicesCtx(ctx context.Context, searchTarget string) ([]MaybeRootDevice, error) {
hc, hcCleanup, err := httpuClient()
if err != nil {
return nil, err
}
defer hcCleanup()
searchCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
responses, err := ssdp.RawSearch(searchCtx, hc, string(searchTarget), 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 := DeviceByURLCtx(ctx, loc); err != nil {
maybe.Err = err
} else {
maybe.Root = root
}
if i := response.Header.Get(httpu.LocalAddressHeader); len(i) > 0 {
maybe.LocalAddr = net.ParseIP(i)
}
}
return results, nil
}
// DiscoverDevices is the legacy version of DiscoverDevicesCtx, but uses
// context.Background() as the context.
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
return DiscoverDevicesCtx(context.Background(), searchTarget)
}
func DeviceByURLCtx(ctx context.Context, loc *url.URL) (*RootDevice, error) {
locStr := loc.String()
root := new(RootDevice)
if err := requestXml(ctx, 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 DeviceByURL(loc *url.URL) (*RootDevice, error) {
return DeviceByURLCtx(context.Background(), loc)
}
// CharsetReaderDefault specifies the charset reader used while decoding the output
// from a UPnP server. It can be modified in an init function to allow for non-utf8 encodings,
// but should not be changed after requesting clients.
var CharsetReaderDefault func(charset string, input io.Reader) (io.Reader, error)
// HTTPClient specifies the http.Client object used when fetching the XML from the UPnP server.
// HTTPClient defaults the http.DefaultClient. This may be overridden by the importing application.
var HTTPClientDefault = http.DefaultClient
func requestXml(ctx context.Context, url string, defaultSpace string, doc interface{}) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := HTTPClientDefault.Do(req)
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 = CharsetReaderDefault
return decoder.Decode(doc)
}

8
vendor/github.com/huin/goupnp/goupnp.sublime-project generated vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"folders":
[
{
"path": "."
}
]
}

218
vendor/github.com/huin/goupnp/httpu/httpu.go generated vendored Normal file
View File

@@ -0,0 +1,218 @@
package httpu
import (
"bufio"
"bytes"
"context"
"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)
}
// ClientInterfaceCtx is the equivalent of ClientInterface, except with methods
// taking a context.Context parameter.
type ClientInterfaceCtx interface {
// DoWithContext performs a request. If the input request has a
// deadline, then that value will be used as the timeout for how long
// to wait before returning the responses that were received. If the
// request's context is canceled, this method will return immediately.
//
// If the request's context is never canceled, and does not have a
// deadline, then this function WILL NEVER RETURN. You MUST set an
// appropriate deadline on the context, or otherwise cancel it when you
// want to finish an operation.
//
// An error is only returned for failing to send the request. Failures
// in receipt simply do not add to the resulting responses.
DoWithContext(
req *http.Request,
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{}
var _ ClientInterfaceCtx = &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) {
ctx := req.Context()
if timeout > 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
req = req.WithContext(ctx)
}
return httpu.DoWithContext(req, numSends)
}
// DoWithContext implements ClientInterfaceCtx.DoWithContext.
//
// Make sure to read the documentation on the ClientInterfaceCtx interface
// regarding cancellation!
func (httpu *HTTPUClient) DoWithContext(
req *http.Request,
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
}
// Handle context deadline/timeout
ctx := req.Context()
deadline, ok := ctx.Deadline()
if ok {
if err = httpu.conn.SetDeadline(deadline); err != nil {
return nil, err
}
}
// Handle context cancelation
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
// if context is cancelled, stop any connections by setting time in the past.
httpu.conn.SetDeadline(time.Now().Add(-time.Second))
case <-done:
}
}()
// 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
}
// Set the related local address used to discover the device.
if a, ok := httpu.conn.LocalAddr().(*net.UDPAddr); ok {
response.Header.Add(LocalAddressHeader, a.IP.String())
}
responses = append(responses, response)
}
// Timeout reached - return discovered responses.
return responses, nil
}
const LocalAddressHeader = "goupnp-local-address"

132
vendor/github.com/huin/goupnp/httpu/multiclient.go generated vendored Normal file
View File

@@ -0,0 +1,132 @@
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()
}
// MultiClientCtx dispatches requests out to all the delegated clients.
type MultiClientCtx struct {
// The HTTPU clients to delegate to.
delegates []ClientInterfaceCtx
}
var _ ClientInterfaceCtx = &MultiClientCtx{}
// NewMultiClient creates a new MultiClient that delegates to all the given
// clients.
func NewMultiClientCtx(delegates []ClientInterfaceCtx) *MultiClientCtx {
return &MultiClientCtx{
delegates: delegates,
}
}
// DoWithContext implements ClientInterfaceCtx.DoWithContext.
func (mc *MultiClientCtx) DoWithContext(
req *http.Request,
numSends int,
) ([]*http.Response, error) {
tasks, ctx := errgroup.WithContext(req.Context())
req = req.WithContext(ctx) // so we cancel if the errgroup errors
results := make(chan []*http.Response)
// For each client, send the request to it and collect results.
tasks.Go(func() error {
defer close(results)
return mc.sendRequestsCtx(results, req, 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 *MultiClientCtx) sendRequestsCtx(
results chan<- []*http.Response,
req *http.Request,
numSends int,
) error {
tasks := &errgroup.Group{}
for _, d := range mc.delegates {
d := d // copy for closure
tasks.Go(func() error {
responses, err := d.DoWithContext(req, numSends)
if err != nil {
return err
}
results <- responses
return nil
})
}
return tasks.Wait()
}

114
vendor/github.com/huin/goupnp/httpu/serve.go generated vendored Normal file
View File

@@ -0,0 +1,114 @@
package httpu
import (
"bufio"
"bytes"
"log"
"net"
"net/http"
"regexp"
"sync"
)
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
}
bufPool := &sync.Pool{
New: func() interface{} {
return make([]byte, maxMessageBytes)
},
}
for {
buf := bufPool.Get().([]byte)
n, peerAddr, err := l.ReadFrom(buf)
if err != nil {
return err
}
go func() {
defer bufPool.Put(buf)
// At least one router's UPnP implementation has added a trailing space
// after "HTTP/1.1" - trim it.
reqBuf := trailingWhitespaceRx.ReplaceAllLiteral(buf[:n], crlf)
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqBuf)))
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.
}()
}
}
// 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)
}

75
vendor/github.com/huin/goupnp/network.go generated vendored Normal file
View 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.ClientInterfaceCtx, 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.ClientInterfaceCtx, 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.NewMultiClientCtx(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 || 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
}

176
vendor/github.com/huin/goupnp/scpd/scpd.go generated vendored Normal file
View File

@@ -0,0 +1,176 @@
package scpd
import (
"encoding/xml"
"sort"
"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) OrderedActions() []Action {
actions := append([]Action{}, scpd.Actions...)
sort.SliceStable(actions, func(i, j int) bool {
return actions[i].Name < actions[j].Name
})
return actions
}
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)
}

118
vendor/github.com/huin/goupnp/service_client.go generated vendored Normal file
View File

@@ -0,0 +1,118 @@
package goupnp
import (
"context"
"fmt"
"net"
"net/url"
"github.com/huin/goupnp/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
localAddr net.IP
}
// NewServiceClientsCtx 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 NewServiceClientsCtx(ctx context.Context, searchTarget string) (clients []ServiceClient, errors []error, err error) {
var maybeRootDevices []MaybeRootDevice
if maybeRootDevices, err = DiscoverDevicesCtx(ctx, 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, maybeRootDevice.LocalAddr)
if err != nil {
errors = append(errors, err)
continue
}
clients = append(clients, deviceClients...)
}
return
}
// NewServiceClients is the legacy version of NewServiceClientsCtx, but uses
// context.Background() as the context.
func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []error, err error) {
return NewServiceClientsCtx(context.Background(), searchTarget)
}
// NewServiceClientsByURLCtx creates client(s) for the given service URN, for a
// root device at the given URL.
func NewServiceClientsByURLCtx(ctx context.Context, loc *url.URL, searchTarget string) ([]ServiceClient, error) {
rootDevice, err := DeviceByURLCtx(ctx, loc)
if err != nil {
return nil, err
}
return NewServiceClientsFromRootDevice(rootDevice, loc, searchTarget)
}
// NewServiceClientsByURL is the legacy version of NewServiceClientsByURLCtx, but uses
// context.Background() as the context.
func NewServiceClientsByURL(loc *url.URL, searchTarget string) ([]ServiceClient, error) {
return NewServiceClientsByURLCtx(context.Background(), 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) {
return newServiceClientsFromRootDevice(rootDevice, loc, searchTarget, nil)
}
func newServiceClientsFromRootDevice(
rootDevice *RootDevice,
loc *url.URL,
searchTarget string,
lAddr net.IP,
) ([]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,
localAddr: lAddr,
})
}
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
}
// LocalAddr returns the address from which the device was discovered (if known - otherwise empty).
func (client *ServiceClient) LocalAddr() net.IP {
return client.localAddr
}

211
vendor/github.com/huin/goupnp/soap/soap.go generated vendored Normal file
View File

@@ -0,0 +1,211 @@
// 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) PerformActionCtx(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)),
}
req = req.WithContext(ctx)
response, err := client.HTTPClient.Do(req)
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
}
// PerformAction is the legacy version of PerformActionCtx, which uses
// context.Background.
func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
return client.PerformActionCtx(context.Background(), actionNamespace, actionName, inAction, outAction)
}
// 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 "&lt;"
case ">":
return "&gt;"
case "&":
return "&amp;"
}
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 {
UPnPError struct {
Errorcode int `xml:"errorCode"`
ErrorDescription string `xml:"errorDescription"`
} `xml:"UPnPError"`
Raw []byte `xml:",innerxml"`
} `xml:"detail"`
}
func (err *SOAPFaultError) Error() string {
return fmt.Sprintf("SOAP fault. Code: %s | Explanation: %s | Detail: %s",
err.FaultCode, err.FaultString, string(err.Detail.Raw))
}

578
vendor/github.com/huin/goupnp/soap/types.go generated vendored Normal file
View File

@@ -0,0 +1,578 @@
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
}
var dateFmts = []string{"2006-01-02", "20060102"}
// 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)
}
// TypeData provides metadata about for marshalling and unmarshalling a SOAP
// type.
type TypeData struct {
funcSuffix string
goType string
}
// GoTypeName returns the name of the Go type.
func (td TypeData) GoTypeName() string {
return td.goType
}
// MarshalFunc returns the name of the function that marshals the type.
func (td TypeData) MarshalFunc() string {
return fmt.Sprintf("Marshal%s", td.funcSuffix)
}
// UnmarshalFunc returns the name of the function that unmarshals the type.
func (td TypeData) UnmarshalFunc() string {
return fmt.Sprintf("Unmarshal%s", td.funcSuffix)
}
// TypeDataMap maps from a SOAP type (e.g "fixed.14.4") to its type data.
var TypeDataMap = map[string]TypeData{
"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"},
}

312
vendor/github.com/huin/goupnp/ssdp/registry.go generated vendored Normal file
View File

@@ -0,0 +1,312 @@
package ssdp
import (
"fmt"
"log"
"net/http"
"net/url"
"regexp"
"strconv"
"sync"
"time"
"github.com/huin/goupnp/httpu"
)
const (
maxExpiryTimeSeconds = 24 * 60 * 60
)
var (
maxAgeRx = regexp.MustCompile("max-age= *([0-9]+)")
)
const (
EventAlive = EventType(iota)
EventUpdate
EventByeBye
)
type EventType int8
func (et EventType) String() string {
switch et {
case EventAlive:
return "EventAlive"
case EventUpdate:
return "EventUpdate"
case EventByeBye:
return "EventByeBye"
default:
return fmt.Sprintf("EventUnknown(%d)", int8(et))
}
}
type Update struct {
// The USN of the service.
USN string
// What happened.
EventType EventType
// The entry, which is nil if the service was not known and
// EventType==EventByeBye. The contents of this must not be modified as it is
// shared with the registry and other listeners. Once created, the Registry
// does not modify the Entry value - any updates are replaced with a new
// Entry value.
Entry *Entry
}
type Entry struct {
// The address that the entry data was actually received from.
RemoteAddr string
// Unique Service Name. Identifies a unique instance of a device or service.
USN string
// Notfication Type. The type of device or service being announced.
NT string
// Server's self-identifying string.
Server string
Host string
// Location of the UPnP root device description.
Location url.URL
// Despite BOOTID,CONFIGID being required fields, apparently they are not
// always set by devices. Set to -1 if not present.
BootID int32
ConfigID int32
SearchPort uint16
// When the last update was received for this entry identified by this USN.
LastUpdate time.Time
// When the last update's cached values are advised to expire.
CacheExpiry time.Time
}
func newEntryFromRequest(r *http.Request) (*Entry, error) {
now := time.Now()
expiryDuration, err := parseCacheControlMaxAge(r.Header.Get("CACHE-CONTROL"))
if err != nil {
return nil, fmt.Errorf("ssdp: error parsing CACHE-CONTROL max age: %v", err)
}
loc, err := url.Parse(r.Header.Get("LOCATION"))
if err != nil {
return nil, fmt.Errorf("ssdp: error parsing entry Location URL: %v", err)
}
bootID, err := parseUpnpIntHeader(r.Header, "BOOTID.UPNP.ORG", -1)
if err != nil {
return nil, err
}
configID, err := parseUpnpIntHeader(r.Header, "CONFIGID.UPNP.ORG", -1)
if err != nil {
return nil, err
}
searchPort, err := parseUpnpIntHeader(r.Header, "SEARCHPORT.UPNP.ORG", ssdpSearchPort)
if err != nil {
return nil, err
}
if searchPort < 1 || searchPort > 65535 {
return nil, fmt.Errorf("ssdp: search port %d is out of range", searchPort)
}
return &Entry{
RemoteAddr: r.RemoteAddr,
USN: r.Header.Get("USN"),
NT: r.Header.Get("NT"),
Server: r.Header.Get("SERVER"),
Host: r.Header.Get("HOST"),
Location: *loc,
BootID: bootID,
ConfigID: configID,
SearchPort: uint16(searchPort),
LastUpdate: now,
CacheExpiry: now.Add(expiryDuration),
}, nil
}
func parseCacheControlMaxAge(cc string) (time.Duration, error) {
matches := maxAgeRx.FindStringSubmatch(cc)
if len(matches) != 2 {
return 0, fmt.Errorf("did not find exactly one max-age in cache control header: %q", cc)
}
expirySeconds, err := strconv.ParseInt(matches[1], 10, 16)
if err != nil {
return 0, err
}
if expirySeconds < 1 || expirySeconds > maxExpiryTimeSeconds {
return 0, fmt.Errorf("rejecting bad expiry time of %d seconds", expirySeconds)
}
return time.Duration(expirySeconds) * time.Second, nil
}
// parseUpnpIntHeader is intended to parse the
// {BOOT,CONFIGID,SEARCHPORT}.UPNP.ORG header fields. It returns the def if
// the head is empty or missing.
func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int32, error) {
s := headers.Get(headerName)
if s == "" {
return def, nil
}
v, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return 0, fmt.Errorf("ssdp: could not parse header %s: %v", headerName, err)
}
return int32(v), nil
}
var _ httpu.Handler = new(Registry)
// Registry maintains knowledge of discovered devices and services.
//
// NOTE: the interface for this is experimental and may change, or go away
// entirely.
type Registry struct {
lock sync.Mutex
byUSN map[string]*Entry
listenersLock sync.RWMutex
listeners map[chan<- Update]struct{}
}
func NewRegistry() *Registry {
return &Registry{
byUSN: make(map[string]*Entry),
listeners: make(map[chan<- Update]struct{}),
}
}
// NewServerAndRegistry is a convenience function to create a registry, and an
// httpu server to pass it messages. Call ListenAndServe on the server for
// messages to be processed.
func NewServerAndRegistry() (*httpu.Server, *Registry) {
reg := NewRegistry()
srv := &httpu.Server{
Addr: ssdpUDP4Addr,
Multicast: true,
Handler: reg,
}
return srv, reg
}
func (reg *Registry) AddListener(c chan<- Update) {
reg.listenersLock.Lock()
defer reg.listenersLock.Unlock()
reg.listeners[c] = struct{}{}
}
func (reg *Registry) RemoveListener(c chan<- Update) {
reg.listenersLock.Lock()
defer reg.listenersLock.Unlock()
delete(reg.listeners, c)
}
func (reg *Registry) sendUpdate(u Update) {
reg.listenersLock.RLock()
defer reg.listenersLock.RUnlock()
for c := range reg.listeners {
c <- u
}
}
// GetService returns known service (or device) entries for the given service
// URN.
func (reg *Registry) GetService(serviceURN string) []*Entry {
// Currently assumes that the map is small, so we do a linear search rather
// than indexed to avoid maintaining two maps.
var results []*Entry
reg.lock.Lock()
defer reg.lock.Unlock()
for _, entry := range reg.byUSN {
if entry.NT == serviceURN {
results = append(results, entry)
}
}
return results
}
// ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to
// maintain the registry of devices and services.
func (reg *Registry) ServeMessage(r *http.Request) {
if r.Method != methodNotify {
return
}
nts := r.Header.Get("nts")
var err error
switch nts {
case ntsAlive:
err = reg.handleNTSAlive(r)
case ntsUpdate:
err = reg.handleNTSUpdate(r)
case ntsByebye:
err = reg.handleNTSByebye(r)
default:
err = fmt.Errorf("unknown NTS value: %q", nts)
}
if err != nil {
log.Printf("goupnp/ssdp: failed to handle %s message from %s: %v", nts, r.RemoteAddr, err)
}
}
func (reg *Registry) handleNTSAlive(r *http.Request) error {
entry, err := newEntryFromRequest(r)
if err != nil {
return err
}
reg.lock.Lock()
reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventAlive,
Entry: entry,
})
return nil
}
func (reg *Registry) handleNTSUpdate(r *http.Request) error {
entry, err := newEntryFromRequest(r)
if err != nil {
return err
}
nextBootID, err := parseUpnpIntHeader(r.Header, "NEXTBOOTID.UPNP.ORG", -1)
if err != nil {
return err
}
entry.BootID = nextBootID
reg.lock.Lock()
reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventUpdate,
Entry: entry,
})
return nil
}
func (reg *Registry) handleNTSByebye(r *http.Request) error {
usn := r.Header.Get("USN")
reg.lock.Lock()
entry := reg.byUSN[usn]
delete(reg.byUSN, usn)
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: usn,
EventType: EventByeBye,
Entry: entry,
})
return nil
}

174
vendor/github.com/huin/goupnp/ssdp/ssdp.go generated vendored Normal file
View File

@@ -0,0 +1,174 @@
package ssdp
import (
"context"
"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)
}
// HTTPUClientCtx is an optional interface that will be used to perform
// HTTP-over-UDP requests if the client implements it.
type HTTPUClientCtx interface {
DoWithContext(
req *http.Request,
numSends int,
) ([]*http.Response, error)
}
// SSDPRawSearchCtx 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 SSDPRawSearchCtx(
ctx context.Context,
httpu HTTPUClient,
searchTarget string,
maxWaitSeconds int,
numSends int,
) ([]*http.Response, error) {
req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
if err != nil {
return nil, err
}
allResponses, err := httpu.Do(req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
if err != nil {
return nil, err
}
return processSSDPResponses(searchTarget, allResponses)
}
// RawSearch 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. If the provided context times out
// or is canceled, the search will be aborted. numSends is the number of
// requests to send - 3 is a reasonable value for this.
//
// The provided context should have a deadline, since the SSDP protocol
// requires the max wait time be included in search requests. If the context
// has no deadline, then a default deadline of 3 seconds will be applied.
func RawSearch(
ctx context.Context,
httpu HTTPUClientCtx,
searchTarget string,
numSends int,
) ([]*http.Response, error) {
// We need a timeout value to include in the SSDP request; get it by
// checking the deadline on the context.
var maxWaitSeconds int
if deadline, ok := ctx.Deadline(); ok {
maxWaitSeconds = int(deadline.Sub(time.Now()) / time.Second)
} else {
// Pick a default timeout of 3 seconds if none was provided.
maxWaitSeconds = 3
var cancel func()
ctx, cancel = context.WithTimeout(ctx, time.Duration(maxWaitSeconds)*time.Second)
defer cancel()
}
req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
if err != nil {
return nil, err
}
allResponses, err := httpu.DoWithContext(req, numSends)
if err != nil {
return nil, err
}
return processSSDPResponses(searchTarget, allResponses)
}
// prepareRequest checks the provided parameters and constructs a SSDP search
// request to be sent.
func prepareRequest(ctx context.Context, searchTarget string, maxWaitSeconds int) (*http.Request, error) {
if maxWaitSeconds < 1 {
return nil, errors.New("ssdp: request timeout must be at least 1s")
}
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},
},
}).WithContext(ctx)
return req, nil
}
func processSSDPResponses(
searchTarget string,
allResponses []*http.Response,
) ([]*http.Response, error) {
isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
seenIDs := 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")
loc, err := response.Location()
if err != nil {
// No usable location in search response - discard.
continue
}
id := loc.String() + "\x00" + usn
if _, alreadySeen := seenIDs[id]; !alreadySeen {
seenIDs[id] = true
responses = append(responses, response)
}
}
return responses, nil
}
// SSDPRawSearch is the legacy version of SSDPRawSearchCtx, but uses
// context.Background() as the context.
func SSDPRawSearch(httpu HTTPUClient, searchTarget string, maxWaitSeconds int, numSends int) ([]*http.Response, error) {
return SSDPRawSearchCtx(context.Background(), httpu, searchTarget, maxWaitSeconds, numSends)
}

11
vendor/github.com/huin/goupnp/workspace.code-workspace generated vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "v2alpha"
}
],
"settings": {}
}