ssdp.go 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. package ssdp
  2. import (
  3. "errors"
  4. "log"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "time"
  9. "github.com/huin/goupnp/httpu"
  10. )
  11. const (
  12. ssdpDiscover = `"ssdp:discover"`
  13. ntsAlive = `ssdp:alive`
  14. ntsByebye = `ssdp:byebye`
  15. ntsUpdate = `ssdp:update`
  16. ssdpUDP4Addr = "239.255.255.250:1900"
  17. ssdpSearchPort = 1900
  18. methodSearch = "M-SEARCH"
  19. methodNotify = "NOTIFY"
  20. )
  21. // SSDPRawSearch performs a fairly raw SSDP search request, and returns the
  22. // unique response(s) that it receives. Each response has the requested
  23. // searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
  24. // wait for responses in seconds, and must be a minimum of 1 (the
  25. // implementation waits an additional 100ms for responses to arrive), 2 is a
  26. // reasonable value for this. numSends is the number of requests to send - 3 is
  27. // a reasonable value for this.
  28. func SSDPRawSearch(httpu *httpu.HTTPUClient, searchTarget string, maxWaitSeconds int, numSends int) ([]*http.Response, error) {
  29. if maxWaitSeconds < 1 {
  30. return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
  31. }
  32. seenUsns := make(map[string]bool)
  33. var responses []*http.Response
  34. req := http.Request{
  35. Method: methodSearch,
  36. // TODO: Support both IPv4 and IPv6.
  37. Host: ssdpUDP4Addr,
  38. URL: &url.URL{Opaque: "*"},
  39. Header: http.Header{
  40. // Putting headers in here avoids them being title-cased.
  41. // (The UPnP discovery protocol uses case-sensitive headers)
  42. "HOST": []string{ssdpUDP4Addr},
  43. "MX": []string{strconv.FormatInt(int64(maxWaitSeconds), 10)},
  44. "MAN": []string{ssdpDiscover},
  45. "ST": []string{searchTarget},
  46. },
  47. }
  48. allResponses, err := httpu.Do(&req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
  49. if err != nil {
  50. return nil, err
  51. }
  52. for _, response := range allResponses {
  53. if response.StatusCode != 200 {
  54. log.Printf("ssdp: got response status code %q in search response", response.Status)
  55. continue
  56. }
  57. if st := response.Header.Get("ST"); st != searchTarget {
  58. log.Printf("ssdp: got unexpected search target result %q", st)
  59. continue
  60. }
  61. location, err := response.Location()
  62. if err != nil {
  63. log.Printf("ssdp: no usable location in search response (discarding): %v", err)
  64. continue
  65. }
  66. usn := response.Header.Get("USN")
  67. if usn == "" {
  68. log.Printf("ssdp: empty/missing USN in search response (using location instead): %v", err)
  69. usn = location.String()
  70. }
  71. if _, alreadySeen := seenUsns[usn]; !alreadySeen {
  72. seenUsns[usn] = true
  73. responses = append(responses, response)
  74. }
  75. }
  76. return responses, nil
  77. }