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 }