149 lines
3.4 KiB
Go
149 lines
3.4 KiB
Go
package ssdp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// 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 searchs services by SSDP.
|
|
func Search(searchType string, waitSec int, localAddr string) ([]Service, error) {
|
|
// dial multicast UDP packet.
|
|
conn, err := multicastListen(localAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
logf("search on %s", conn.LocalAddr().String())
|
|
|
|
// send request.
|
|
msg, err := buildSearch(ssdpAddrIPv4, searchType, waitSec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := conn.WriteTo(msg, ssdpAddrIPv4); 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 {
|
|
logf("invalid search response from %s: %s", a.String(), err)
|
|
return nil
|
|
}
|
|
list = append(list, *srv)
|
|
logf("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")
|
|
)
|
|
|
|
// FIXME: https://github.com/koron/go-ssdp/issues/10
|
|
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
|
|
}
|