215 lines
4.6 KiB
Go
215 lines
4.6 KiB
Go
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)
|