123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- 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
- }
|