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:
312
vendor/github.com/huin/goupnp/ssdp/registry.go
generated
vendored
Normal file
312
vendor/github.com/huin/goupnp/ssdp/registry.go
generated
vendored
Normal 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
174
vendor/github.com/huin/goupnp/ssdp/ssdp.go
generated
vendored
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user