aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/huin/goupnp/ssdp/registry.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/huin/goupnp/ssdp/registry.go')
-rw-r--r--vendor/github.com/huin/goupnp/ssdp/registry.go312
1 files changed, 312 insertions, 0 deletions
diff --git a/vendor/github.com/huin/goupnp/ssdp/registry.go b/vendor/github.com/huin/goupnp/ssdp/registry.go
new file mode 100644
index 000000000..2f84beaae
--- /dev/null
+++ b/vendor/github.com/huin/goupnp/ssdp/registry.go
@@ -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
+}