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/koron/go-ssdp/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
*.exe
tags
tmp/

21
vendor/github.com/koron/go-ssdp/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 MURAOKA Taro <koron.kaoriya@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

71
vendor/github.com/koron/go-ssdp/Makefile generated vendored Normal file
View File

@@ -0,0 +1,71 @@
EXAMPLES = advertise alive bye monitor search
.PHONY: build
build:
go build -gcflags '-e'
.PHONY: test
test:
go test -gcflags '-e' ./...
.PHONY: bench
bench:
go test -bench ./...
.PHONY: tags
tags:
gotags -f tags -R .
.PHONY: cover
cover:
mkdir -p tmp
go test -coverprofile tmp/_cover.out . ./internal/...
go tool cover -html tmp/_cover.out -o tmp/cover.html
.PHONY: checkall
checkall: vet staticcheck
.PHONY: vet
vet:
go vet ./...
.PHONY: staticcheck
staticcheck:
staticcheck ./...
.PHONY: clean
clean: examples-clean
go clean
rm -f tags
rm -f tmp/_cover.out tmp/cover.html
# based on: github.com/koron-go/_skeleton/Makefile
.PHONY: test-race
test-race:
go test -race .
.PHONY: examples
examples: examples-build
.PHONY: examples-build
examples-build: $(EXAMPLES)
.PHONY: examples-clean
examples-clean:
rm -f $(EXAMPLES)
advertise: examples/advertise/*.go *.go
go build ./examples/advertise
alive: examples/alive/*.go *.go
go build ./examples/alive
bye: examples/bye/*.go *.go
go build ./examples/bye
monitor: examples/monitor/*.go *.go
go build ./examples/monitor
search: examples/search/*.go *.go
go build ./examples/search

95
vendor/github.com/koron/go-ssdp/README.md generated vendored Normal file
View File

@@ -0,0 +1,95 @@
# SSDP library
[![GoDoc](https://godoc.org/github.com/koron/go-ssdp?status.svg)](https://godoc.org/github.com/koron/go-ssdp)
[![Actions/Go](https://github.com/koron/go-ssdp/workflows/Go/badge.svg)](https://github.com/koron/go-ssdp/actions?query=workflow%3AGo)
[![Go Report Card](https://goreportcard.com/badge/github.com/koron/go-ssdp)](https://goreportcard.com/report/github.com/koron/go-ssdp)
Based on <https://tools.ietf.org/html/draft-cai-ssdp-v1-03>.
## Examples
There are tiny snippets for example. See also examples/ directory for working
examples.
### Respond to search
```go
import "github.com/koron/go-ssdp"
ad, err := ssdp.Advertise(
"my:device", // send as "ST"
"unique:id", // send as "USN"
"http://192.168.0.1:57086/foo.xml", // send as "LOCATION"
"go-ssdp sample", // send as "SERVER"
1800) // send as "maxAge" in "CACHE-CONTROL"
if err != nil {
panic(err)
}
// run Advertiser infinitely.
quit := make(chan bool)
<-quit
```
### Send alive periodically
```go
import "time"
aliveTick := time.Tick(300 * time.Second)
for {
select {
case <-aliveTick:
ad.Alive()
}
}
```
### Send bye when quiting
```go
import (
"os"
"os/signal"
)
// to detect CTRL-C is pressed.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
loop:
for {
select {
case <-aliveTick:
ad.Alive()
case <-quit:
break loop
}
}
// send/multicast "byebye" message.
ad.Bye()
// teminate Advertiser.
ad.Close()
```
### Limitate interfaces to multicast
go-ssdp will send multicast messages to all IPv4 interfaces as default.
When you want to limitate interfaces, see below snippet.
```go
import (
"github.com/koron/go-ssdp"
"net"
)
en0, err := net.InterfaceByName("en0")
if err != nil {
panic(err)
}
ssdp.Interfaces = []net.Interface{*en0}
```
go-ssdp will send multicast message only "en0" after this.

185
vendor/github.com/koron/go-ssdp/advertise.go generated vendored Normal file
View File

@@ -0,0 +1,185 @@
package ssdp
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/http"
"sync"
"github.com/koron/go-ssdp/internal/multicast"
"github.com/koron/go-ssdp/internal/ssdplog"
)
type message struct {
to net.Addr
data multicast.DataProvider
}
// Advertiser is a server to advertise a service.
type Advertiser struct {
st string
usn string
locProv LocationProvider
server string
maxAge int
conn *multicast.Conn
ch chan *message
wg sync.WaitGroup
wgS sync.WaitGroup
}
// Advertise starts advertisement of service.
// location should be a string or a ssdp.LocationProvider.
func Advertise(st, usn string, location interface{}, server string, maxAge int) (*Advertiser, error) {
locProv, err := toLocationProvider(location)
if err != nil {
return nil, err
}
conn, err := multicast.Listen(multicast.RecvAddrResolver)
if err != nil {
return nil, err
}
ssdplog.Printf("SSDP advertise on: %s", conn.LocalAddr().String())
a := &Advertiser{
st: st,
usn: usn,
locProv: locProv,
server: server,
maxAge: maxAge,
conn: conn,
ch: make(chan *message),
}
a.wg.Add(2)
a.wgS.Add(1)
go func() {
a.sendMain()
a.wgS.Done()
a.wg.Done()
}()
go func() {
a.recvMain()
a.wg.Done()
}()
return a, nil
}
func (a *Advertiser) recvMain() error {
// TODO: update listening interfaces of a.conn
err := a.conn.ReadPackets(0, func(addr net.Addr, data []byte) error {
if err := a.handleRaw(addr, data); err != nil {
ssdplog.Printf("failed to handle message: %s", err)
}
return nil
})
if err != nil && err != io.EOF {
return err
}
return nil
}
func (a *Advertiser) sendMain() {
for msg := range a.ch {
_, err := a.conn.WriteTo(msg.data, msg.to)
if err != nil {
ssdplog.Printf("failed to send: %s", err)
}
}
}
func (a *Advertiser) handleRaw(from net.Addr, raw []byte) error {
if !bytes.HasPrefix(raw, []byte("M-SEARCH ")) {
// unexpected method.
return nil
}
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
if err != nil {
return err
}
var (
man = req.Header.Get("MAN")
st = req.Header.Get("ST")
)
if man != `"ssdp:discover"` {
return fmt.Errorf("unexpected MAN: %s", man)
}
if st != All && st != RootDevice && st != a.st {
// skip when ST is not matched/expected.
return nil
}
ssdplog.Printf("received M-SEARCH MAN=%s ST=%s from %s", man, st, from.String())
// build and send a response.
msg := buildOK(a.st, a.usn, a.locProv.Location(from, nil), a.server, a.maxAge)
a.ch <- &message{to: from, data: multicast.BytesDataProvider(msg)}
return nil
}
func buildOK(st, usn, location, server string, maxAge int) []byte {
// bytes.Buffer#Write() is never fail, so we can omit error checks.
b := new(bytes.Buffer)
b.WriteString("HTTP/1.1 200 OK\r\n")
fmt.Fprintf(b, "EXT: \r\n")
fmt.Fprintf(b, "ST: %s\r\n", st)
fmt.Fprintf(b, "USN: %s\r\n", usn)
if location != "" {
fmt.Fprintf(b, "LOCATION: %s\r\n", location)
}
if server != "" {
fmt.Fprintf(b, "SERVER: %s\r\n", server)
}
fmt.Fprintf(b, "CACHE-CONTROL: max-age=%d\r\n", maxAge)
b.WriteString("\r\n")
return b.Bytes()
}
// Close stops advertisement.
func (a *Advertiser) Close() error {
if a.conn != nil {
// closing order is very important. be careful to change:
// stop sending loop by closing the channel and wait it.
close(a.ch)
a.wgS.Wait()
// stop receiving loop by closing the connection.
a.conn.Close()
a.wg.Wait()
a.conn = nil
}
return nil
}
// Alive announces ssdp:alive message.
func (a *Advertiser) Alive() error {
addr, err := multicast.SendAddr()
if err != nil {
return err
}
msg := &aliveDataProvider{
host: addr,
nt: a.st,
usn: a.usn,
location: a.locProv,
server: a.server,
maxAge: a.maxAge,
}
a.ch <- &message{to: addr, data: msg}
ssdplog.Printf("sent alive")
return nil
}
// Bye announces ssdp:byebye message.
func (a *Advertiser) Bye() error {
addr, err := multicast.SendAddr()
if err != nil {
return err
}
msg, err := buildBye(addr, a.st, a.usn)
if err != nil {
return err
}
a.ch <- &message{to: addr, data: multicast.BytesDataProvider(msg)}
ssdplog.Printf("sent bye")
return nil
}

110
vendor/github.com/koron/go-ssdp/announce.go generated vendored Normal file
View File

@@ -0,0 +1,110 @@
package ssdp
import (
"bytes"
"fmt"
"net"
"github.com/koron/go-ssdp/internal/multicast"
)
// AnnounceAlive sends ssdp:alive message.
// location should be a string or a ssdp.LocationProvider.
func AnnounceAlive(nt, usn string, location interface{}, server string, maxAge int, localAddr string) error {
locProv, err := toLocationProvider(location)
if err != nil {
return err
}
// dial multicast UDP packet.
conn, err := multicast.Listen(&multicast.AddrResolver{Addr: localAddr})
if err != nil {
return err
}
defer conn.Close()
// build and send message.
addr, err := multicast.SendAddr()
if err != nil {
return err
}
msg := &aliveDataProvider{
host: addr,
nt: nt,
usn: usn,
location: locProv,
server: server,
maxAge: maxAge,
}
if _, err := conn.WriteTo(msg, addr); err != nil {
return err
}
return nil
}
type aliveDataProvider struct {
host net.Addr
nt string
usn string
location LocationProvider
server string
maxAge int
}
func (p *aliveDataProvider) Bytes(ifi *net.Interface) []byte {
return buildAlive(p.host, p.nt, p.usn, p.location.Location(nil, ifi), p.server, p.maxAge)
}
var _ multicast.DataProvider = (*aliveDataProvider)(nil)
func buildAlive(raddr net.Addr, nt, usn, location, server string, maxAge int) []byte {
// bytes.Buffer#Write() is never fail, so we can omit error checks.
b := new(bytes.Buffer)
b.WriteString("NOTIFY * HTTP/1.1\r\n")
fmt.Fprintf(b, "HOST: %s\r\n", raddr.String())
fmt.Fprintf(b, "NT: %s\r\n", nt)
fmt.Fprintf(b, "NTS: %s\r\n", "ssdp:alive")
fmt.Fprintf(b, "USN: %s\r\n", usn)
if location != "" {
fmt.Fprintf(b, "LOCATION: %s\r\n", location)
}
if server != "" {
fmt.Fprintf(b, "SERVER: %s\r\n", server)
}
fmt.Fprintf(b, "CACHE-CONTROL: max-age=%d\r\n", maxAge)
b.WriteString("\r\n")
return b.Bytes()
}
// AnnounceBye sends ssdp:byebye message.
func AnnounceBye(nt, usn, localAddr string) error {
// dial multicast UDP packet.
conn, err := multicast.Listen(&multicast.AddrResolver{Addr: localAddr})
if err != nil {
return err
}
defer conn.Close()
// build and send message.
addr, err := multicast.SendAddr()
if err != nil {
return err
}
msg, err := buildBye(addr, nt, usn)
if err != nil {
return err
}
if _, err := conn.WriteTo(multicast.BytesDataProvider(msg), addr); err != nil {
return err
}
return nil
}
func buildBye(raddr net.Addr, nt, usn string) ([]byte, error) {
b := new(bytes.Buffer)
// FIXME: error should be checked.
b.WriteString("NOTIFY * HTTP/1.1\r\n")
fmt.Fprintf(b, "HOST: %s\r\n", raddr.String())
fmt.Fprintf(b, "NT: %s\r\n", nt)
fmt.Fprintf(b, "NTS: %s\r\n", "ssdp:byebye")
fmt.Fprintf(b, "USN: %s\r\n", usn)
b.WriteString("\r\n")
return b.Bytes(), nil
}

4
vendor/github.com/koron/go-ssdp/doc.go generated vendored Normal file
View File

@@ -0,0 +1,4 @@
/*
Package ssdp provides SSDP advertiser or so.
*/
package ssdp

View File

@@ -0,0 +1,4 @@
/*
Package multicast provides utilities for network multicast.
*/
package multicast

View File

@@ -0,0 +1,65 @@
package multicast
import (
"net"
)
type InterfacesProviderFunc func() []net.Interface
// InterfacesProvider specify a function to list all interfaces to multicast.
// If no provider are given, all possible interfaces will be used.
var InterfacesProvider InterfacesProviderFunc
// interfaces gets list of net.Interface to multicast UDP packet.
func interfaces() ([]net.Interface, error) {
if p := InterfacesProvider; p != nil {
if list := p(); len(list) > 0 {
return list, nil
}
}
return interfacesIPv4()
}
// interfacesIPv4 lists net.Interface on IPv4.
func interfacesIPv4() ([]net.Interface, error) {
iflist, err := net.Interfaces()
if err != nil {
return nil, err
}
list := make([]net.Interface, 0, len(iflist))
for _, ifi := range iflist {
if !hasLinkUp(&ifi) || !hasMulticast(&ifi) || !hasIPv4Address(&ifi) {
continue
}
list = append(list, ifi)
}
return list, nil
}
// hasLinkUp checks an I/F have link-up or not.
func hasLinkUp(ifi *net.Interface) bool {
return ifi.Flags&net.FlagUp != 0
}
// hasMulticast checks an I/F supports multicast or not.
func hasMulticast(ifi *net.Interface) bool {
return ifi.Flags&net.FlagMulticast != 0
}
// hasIPv4Address checks an I/F have IPv4 address.
func hasIPv4Address(ifi *net.Interface) bool {
addrs, err := ifi.Addrs()
if err != nil {
return false
}
for _, a := range addrs {
ip, _, err := net.ParseCIDR(a.String())
if err != nil {
continue
}
if len(ip.To4()) == net.IPv4len && !ip.IsUnspecified() {
return true
}
}
return false
}

View File

@@ -0,0 +1,155 @@
package multicast
import (
"errors"
"io"
"net"
"strings"
"time"
"github.com/koron/go-ssdp/internal/ssdplog"
"golang.org/x/net/ipv4"
)
// Conn is multicast connection.
type Conn struct {
laddr *net.UDPAddr
conn *net.UDPConn
pconn *ipv4.PacketConn
iflist []net.Interface
}
// Listen starts to receiving multicast messages.
func Listen(r *AddrResolver) (*Conn, error) {
// prepare parameters.
laddr, err := r.resolve()
if err != nil {
return nil, err
}
// connect.
conn, err := net.ListenUDP("udp4", laddr)
if err != nil {
return nil, err
}
// configure socket to use with multicast.
pconn, iflist, err := newIPv4MulticastConn(conn)
if err != nil {
conn.Close()
return nil, err
}
return &Conn{
laddr: laddr,
conn: conn,
pconn: pconn,
iflist: iflist,
}, nil
}
func newIPv4MulticastConn(conn *net.UDPConn) (*ipv4.PacketConn, []net.Interface, error) {
iflist, err := interfaces()
if err != nil {
return nil, nil, err
}
addr, err := SendAddr()
if err != nil {
return nil, nil, err
}
pconn, err := joinGroupIPv4(conn, iflist, addr)
if err != nil {
return nil, nil, err
}
return pconn, iflist, nil
}
// joinGroupIPv4 makes the connection join to a group on interfaces.
func joinGroupIPv4(conn *net.UDPConn, iflist []net.Interface, gaddr net.Addr) (*ipv4.PacketConn, error) {
wrap := ipv4.NewPacketConn(conn)
wrap.SetMulticastLoopback(true)
// add interfaces to multicast group.
joined := 0
for _, ifi := range iflist {
if err := wrap.JoinGroup(&ifi, gaddr); err != nil {
ssdplog.Printf("failed to join group %s on %s: %s", gaddr.String(), ifi.Name, err)
continue
}
joined++
ssdplog.Printf("joined group %s on %s (#%d)", gaddr.String(), ifi.Name, ifi.Index)
}
if joined == 0 {
return nil, errors.New("no interfaces had joined to group")
}
return wrap, nil
}
// Close closes a multicast connection.
func (mc *Conn) Close() error {
if err := mc.pconn.Close(); err != nil {
return err
}
// mc.conn is closed by mc.pconn.Close()
return nil
}
// DataProvider provides a body of multicast message to send.
type DataProvider interface {
Bytes(*net.Interface) []byte
}
//type multicastDataProviderFunc func(*net.Interface) []byte
//
//func (f multicastDataProviderFunc) Bytes(ifi *net.Interface) []byte {
// return f(ifi)
//}
type BytesDataProvider []byte
func (b BytesDataProvider) Bytes(ifi *net.Interface) []byte {
return []byte(b)
}
// WriteTo sends a multicast message to interfaces.
func (mc *Conn) WriteTo(dataProv DataProvider, to net.Addr) (int, error) {
if uaddr, ok := to.(*net.UDPAddr); ok && !uaddr.IP.IsMulticast() {
return mc.conn.WriteTo(dataProv.Bytes(nil), to)
}
sum := 0
for _, ifi := range mc.iflist {
if err := mc.pconn.SetMulticastInterface(&ifi); err != nil {
return 0, err
}
n, err := mc.pconn.WriteTo(dataProv.Bytes(&ifi), nil, to)
if err != nil {
return 0, err
}
sum += n
}
return sum, nil
}
// LocalAddr returns local address to listen multicast packets.
func (mc *Conn) LocalAddr() net.Addr {
return mc.laddr
}
// ReadPackets reads multicast packets.
func (mc *Conn) ReadPackets(timeout time.Duration, h PacketHandler) error {
buf := make([]byte, 65535)
if timeout > 0 {
mc.pconn.SetReadDeadline(time.Now().Add(timeout))
}
for {
n, _, addr, err := mc.pconn.ReadFrom(buf)
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return nil
}
if strings.Contains(err.Error(), "use of closed network connection") {
return io.EOF
}
return err
}
if err := h(addr, buf[:n]); err != nil {
return err
}
}
}

View File

@@ -0,0 +1,65 @@
package multicast
import (
"net"
"sync"
)
type PacketHandler func(net.Addr, []byte) error
type AddrResolver struct {
Addr string
mu sync.RWMutex
udp *net.UDPAddr
err error
}
func (r *AddrResolver) setAddress(addr string) {
r.mu.Lock()
r.Addr = addr
r.udp = nil
r.err = nil
r.mu.Unlock()
}
func (r *AddrResolver) resolve() (*net.UDPAddr, error) {
r.mu.RLock()
if err := r.err; err != nil {
r.mu.RUnlock()
return nil, err
}
if udp := r.udp; udp != nil {
r.mu.RUnlock()
return udp, nil
}
r.mu.RUnlock()
r.mu.Lock()
defer r.mu.Unlock()
r.udp, r.err = net.ResolveUDPAddr("udp4", r.Addr)
return r.udp, r.err
}
var RecvAddrResolver = &AddrResolver{Addr: "224.0.0.1:1900"}
// SetRecvAddrIPv4 updates multicast address where to receive packets.
// This never fail now.
func SetRecvAddrIPv4(addr string) error {
RecvAddrResolver.setAddress(addr)
return nil
}
var sendAddrResolver = &AddrResolver{Addr: "239.255.255.250:1900"}
// SendAddr returns an address to send multicast UDP package.
func SendAddr() (*net.UDPAddr, error) {
return sendAddrResolver.resolve()
}
// SetSendAddrIPv4 updates a UDP address to send multicast packets.
// This never fail now.
func SetSendAddrIPv4(addr string) error {
sendAddrResolver.setAddress(addr)
return nil
}

View File

@@ -0,0 +1,16 @@
/*
Package ssdplog provides log mechanism for ssdp.
*/
package ssdplog
import "log"
var LoggerProvider = func() *log.Logger { return nil }
func Printf(s string, a ...interface{}) {
if p := LoggerProvider; p != nil {
if l := p(); l != nil {
l.Printf(s, a...)
}
}
}

40
vendor/github.com/koron/go-ssdp/location.go generated vendored Normal file
View File

@@ -0,0 +1,40 @@
package ssdp
import (
"fmt"
"net"
)
// LocationProvider provides address for Location header which can be reached from
// "from" address network.
type LocationProvider interface {
// Location provides an address be reachable from the network located
// by "from" address or "ifi" interface.
// One of "from" or "ifi" must not be nil.
Location(from net.Addr, ifi *net.Interface) string
}
// LocationProviderFunc type is an adapter to allow the use of ordinary
// functions are location providers.
type LocationProviderFunc func(net.Addr, *net.Interface) string
func (f LocationProviderFunc) Location(from net.Addr, ifi *net.Interface) string {
return f(from, ifi)
}
type fixedLocation string
func (s fixedLocation) Location(net.Addr, *net.Interface) string {
return string(s)
}
func toLocationProvider(v interface{}) (LocationProvider, error) {
switch w := v.(type) {
case string:
return fixedLocation(w), nil
case LocationProvider:
return w, nil
default:
return nil, fmt.Errorf("location should be a string or a ssdp.LocationProvider but got %T", w)
}
}

214
vendor/github.com/koron/go-ssdp/monitor.go generated vendored Normal file
View File

@@ -0,0 +1,214 @@
package ssdp
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"github.com/koron/go-ssdp/internal/multicast"
"github.com/koron/go-ssdp/internal/ssdplog"
)
// Monitor monitors SSDP's alive and byebye messages.
type Monitor struct {
Alive AliveHandler
Bye ByeHandler
Search SearchHandler
conn *multicast.Conn
wg sync.WaitGroup
}
// Start starts to monitor SSDP messages.
func (m *Monitor) Start() error {
conn, err := multicast.Listen(multicast.RecvAddrResolver)
if err != nil {
return err
}
ssdplog.Printf("monitoring on %s", conn.LocalAddr().String())
m.conn = conn
m.wg.Add(1)
go func() {
m.serve()
m.wg.Done()
}()
return nil
}
func (m *Monitor) serve() error {
// TODO: update listening interfaces of m.conn
err := m.conn.ReadPackets(0, func(addr net.Addr, data []byte) error {
msg := make([]byte, len(data))
copy(msg, data)
go m.handleRaw(addr, msg)
return nil
})
if err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
}
func (m *Monitor) handleRaw(addr net.Addr, raw []byte) error {
// Add newline to workaround buggy SSDP responses
if !bytes.HasSuffix(raw, endOfHeader) {
raw = bytes.Join([][]byte{raw, endOfHeader}, nil)
}
if bytes.HasPrefix(raw, []byte("M-SEARCH ")) {
return m.handleSearch(addr, raw)
}
if bytes.HasPrefix(raw, []byte("NOTIFY ")) {
return m.handleNotify(addr, raw)
}
n := bytes.Index(raw, []byte("\r\n"))
ssdplog.Printf("unexpected method: %q", string(raw[:n]))
return nil
}
func (m *Monitor) handleNotify(addr net.Addr, raw []byte) error {
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
if err != nil {
return err
}
switch nts := req.Header.Get("NTS"); nts {
case "ssdp:alive":
if req.Method != "NOTIFY" {
return fmt.Errorf("unexpected method for %q: %s", "ssdp:alive", req.Method)
}
if h := m.Alive; h != nil {
h(&AliveMessage{
From: addr,
Type: req.Header.Get("NT"),
USN: req.Header.Get("USN"),
Location: req.Header.Get("LOCATION"),
Server: req.Header.Get("SERVER"),
rawHeader: req.Header,
})
}
case "ssdp:byebye":
if req.Method != "NOTIFY" {
return fmt.Errorf("unexpected method for %q: %s", "ssdp:byebye", req.Method)
}
if h := m.Bye; h != nil {
h(&ByeMessage{
From: addr,
Type: req.Header.Get("NT"),
USN: req.Header.Get("USN"),
rawHeader: req.Header,
})
}
default:
return fmt.Errorf("unknown NTS: %s", nts)
}
return nil
}
func (m *Monitor) handleSearch(addr net.Addr, raw []byte) error {
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
if err != nil {
return err
}
man := req.Header.Get("MAN")
if man != `"ssdp:discover"` {
return fmt.Errorf("unexpected MAN: %s", man)
}
if h := m.Search; h != nil {
h(&SearchMessage{
From: addr,
Type: req.Header.Get("ST"),
rawHeader: req.Header,
})
}
return nil
}
// Close closes monitoring.
func (m *Monitor) Close() error {
if m.conn != nil {
m.conn.Close()
m.conn = nil
m.wg.Wait()
}
return nil
}
// AliveMessage represents SSDP's ssdp:alive message.
type AliveMessage struct {
// From is a sender of this message
From net.Addr
// Type is a property of "NT"
Type string
// USN is a property of "USN"
USN string
// Location is a property of "LOCATION"
Location string
// Server is a property of "SERVER"
Server string
rawHeader http.Header
maxAge *int
}
// Header returns all properties in alive message.
func (m *AliveMessage) Header() http.Header {
return m.rawHeader
}
// MaxAge extracts "max-age" value from "CACHE-CONTROL" property.
func (m *AliveMessage) MaxAge() int {
if m.maxAge == nil {
m.maxAge = new(int)
*m.maxAge = extractMaxAge(m.rawHeader.Get("CACHE-CONTROL"), -1)
}
return *m.maxAge
}
// AliveHandler is handler of Alive message.
type AliveHandler func(*AliveMessage)
// ByeMessage represents SSDP's ssdp:byebye message.
type ByeMessage struct {
// From is a sender of this message
From net.Addr
// Type is a property of "NT"
Type string
// USN is a property of "USN"
USN string
rawHeader http.Header
}
// Header returns all properties in bye message.
func (m *ByeMessage) Header() http.Header {
return m.rawHeader
}
// ByeHandler is handler of Bye message.
type ByeHandler func(*ByeMessage)
// SearchMessage represents SSDP's ssdp:discover message.
type SearchMessage struct {
From net.Addr
Type string
rawHeader http.Header
}
// Header returns all properties in search message.
func (s *SearchMessage) Header() http.Header {
return s.rawHeader
}
// SearchHandler is handler of Search message.
type SearchHandler func(*SearchMessage)

154
vendor/github.com/koron/go-ssdp/search.go generated vendored Normal file
View File

@@ -0,0 +1,154 @@
package ssdp
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"net/http"
"regexp"
"strconv"
"time"
"github.com/koron/go-ssdp/internal/multicast"
"github.com/koron/go-ssdp/internal/ssdplog"
)
// Service is discovered service.
type Service struct {
// Type is a property of "ST"
Type string
// USN is a property of "USN"
USN string
// Location is a property of "LOCATION"
Location string
// Server is a property of "SERVER"
Server string
rawHeader http.Header
maxAge *int
}
var rxMaxAge = regexp.MustCompile(`\bmax-age\s*=\s*(\d+)\b`)
func extractMaxAge(s string, value int) int {
v := value
if m := rxMaxAge.FindStringSubmatch(s); m != nil {
i64, err := strconv.ParseInt(m[1], 10, 32)
if err == nil {
v = int(i64)
}
}
return v
}
// MaxAge extracts "max-age" value from "CACHE-CONTROL" property.
func (s *Service) MaxAge() int {
if s.maxAge == nil {
s.maxAge = new(int)
*s.maxAge = extractMaxAge(s.rawHeader.Get("CACHE-CONTROL"), -1)
}
return *s.maxAge
}
// Header returns all properties in response of search.
func (s *Service) Header() http.Header {
return s.rawHeader
}
const (
// All is a search type to search all services and devices.
All = "ssdp:all"
// RootDevice is a search type to search UPnP root devices.
RootDevice = "upnp:rootdevice"
)
// Search searches services by SSDP.
func Search(searchType string, waitSec int, localAddr string) ([]Service, error) {
// dial multicast UDP packet.
conn, err := multicast.Listen(&multicast.AddrResolver{Addr: localAddr})
if err != nil {
return nil, err
}
defer conn.Close()
ssdplog.Printf("search on %s", conn.LocalAddr().String())
// send request.
addr, err := multicast.SendAddr()
if err != nil {
return nil, err
}
msg, err := buildSearch(addr, searchType, waitSec)
if err != nil {
return nil, err
}
if _, err := conn.WriteTo(multicast.BytesDataProvider(msg), addr); err != nil {
return nil, err
}
// wait response.
var list []Service
h := func(a net.Addr, d []byte) error {
srv, err := parseService(a, d)
if err != nil {
ssdplog.Printf("invalid search response from %s: %s", a.String(), err)
return nil
}
list = append(list, *srv)
ssdplog.Printf("search response from %s: %s", a.String(), srv.USN)
return nil
}
d := time.Second * time.Duration(waitSec)
if err := conn.ReadPackets(d, h); err != nil {
return nil, err
}
return list, err
}
func buildSearch(raddr net.Addr, searchType string, waitSec int) ([]byte, error) {
b := new(bytes.Buffer)
// FIXME: error should be checked.
b.WriteString("M-SEARCH * HTTP/1.1\r\n")
fmt.Fprintf(b, "HOST: %s\r\n", raddr.String())
fmt.Fprintf(b, "MAN: %q\r\n", "ssdp:discover")
fmt.Fprintf(b, "MX: %d\r\n", waitSec)
fmt.Fprintf(b, "ST: %s\r\n", searchType)
b.WriteString("\r\n")
return b.Bytes(), nil
}
var (
errWithoutHTTPPrefix = errors.New("without HTTP prefix")
)
var endOfHeader = []byte{'\r', '\n', '\r', '\n'}
func parseService(addr net.Addr, data []byte) (*Service, error) {
if !bytes.HasPrefix(data, []byte("HTTP")) {
return nil, errWithoutHTTPPrefix
}
// Complement newlines on tail of header for buggy SSDP responses.
if !bytes.HasSuffix(data, endOfHeader) {
// why we should't use append() for this purpose:
// https://play.golang.org/p/IM1pONW9lqm
data = bytes.Join([][]byte{data, endOfHeader}, nil)
}
resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(data)), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return &Service{
Type: resp.Header.Get("ST"),
USN: resp.Header.Get("USN"),
Location: resp.Header.Get("LOCATION"),
Server: resp.Header.Get("SERVER"),
rawHeader: resp.Header,
}, nil
}

37
vendor/github.com/koron/go-ssdp/ssdp.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
package ssdp
import (
"log"
"net"
"github.com/koron/go-ssdp/internal/multicast"
"github.com/koron/go-ssdp/internal/ssdplog"
)
func init() {
multicast.InterfacesProvider = func() []net.Interface {
return Interfaces
}
ssdplog.LoggerProvider = func() *log.Logger {
return Logger
}
}
// Interfaces specify target interfaces to multicast. If no interfaces are
// specified, all interfaces will be used.
var Interfaces []net.Interface
// Logger is default logger for SSDP module.
var Logger *log.Logger
// SetMulticastRecvAddrIPv4 updates multicast address where to receive packets.
// This never fail now.
func SetMulticastRecvAddrIPv4(addr string) error {
return multicast.SetRecvAddrIPv4(addr)
}
// SetMulticastSendAddrIPv4 updates a UDP address to send multicast packets.
// This never fail now.
func SetMulticastSendAddrIPv4(addr string) error {
return multicast.SetSendAddrIPv4(addr)
}

5
vendor/github.com/koron/go-ssdp/staticcheck.conf generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# vim:set ft=toml:
checks = ["all"]
# based on: github.com/koron-go/_skeleton/staticcheck.conf