ssdp.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. package ssdp
  2. import (
  3. "context"
  4. "log"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "time"
  9. )
  10. const (
  11. ssdpDiscover = `"ssdp:discover"`
  12. ntsAlive = `ssdp:alive`
  13. ntsByebye = `ssdp:byebye`
  14. ntsUpdate = `ssdp:update`
  15. ssdpUDP4Addr = "239.255.255.250:1900"
  16. ssdpSearchPort = 1900
  17. methodSearch = "M-SEARCH"
  18. methodNotify = "NOTIFY"
  19. // SSDPAll is a value for searchTarget that searches for all devices and services.
  20. SSDPAll = "ssdp:all"
  21. // UPNPRootDevice is a value for searchTarget that searches for all root devices.
  22. UPNPRootDevice = "upnp:rootdevice"
  23. )
  24. // HTTPUClient is the interface required to perform HTTP-over-UDP requests.
  25. type HTTPUClient interface {
  26. Do(req *http.Request, numSends int) ([]*http.Response, error)
  27. }
  28. func max(a, b int64) int64 {
  29. if a > b {
  30. return a
  31. }
  32. return b
  33. }
  34. // SSDPRawSearch performs a fairly raw SSDP search request, and returns the
  35. // unique response(s) that it receives. Each response has the requested
  36. // searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
  37. // wait for responses in seconds, and must be a minimum of 1 (the
  38. // implementation waits an additional 100ms for responses to arrive), 2 is a
  39. // reasonable value for this. numSends is the number of requests to send - 3 is
  40. // a reasonable value for this.
  41. func SSDPRawSearch(
  42. ctx context.Context,
  43. httpu HTTPUClient,
  44. searchTarget string,
  45. numSends int,
  46. ) ([]*http.Response, error) {
  47. // Must specify at least 1 second according to the spec.
  48. var wait int64 = 1
  49. // https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v2.0-20200417.pdf
  50. if deadline, ok := ctx.Deadline(); ok {
  51. wait = max(wait, int64(time.Until(deadline).Seconds()))
  52. }
  53. header := http.Header{
  54. // Putting headers in here avoids them being title-cased.
  55. // (The UPnP discovery protocol uses case-sensitive headers)
  56. "HOST": []string{ssdpUDP4Addr},
  57. "MAN": []string{ssdpDiscover},
  58. "MX": []string{strconv.FormatInt(wait, 10)},
  59. "ST": []string{searchTarget},
  60. }
  61. req := &http.Request{
  62. Method: methodSearch,
  63. // TODO: Support both IPv4 and IPv6.
  64. Host: ssdpUDP4Addr,
  65. URL: &url.URL{Opaque: "*"},
  66. Header: header,
  67. }
  68. ctx, cancel := context.WithTimeout(ctx, time.Duration(wait)*time.Second)
  69. defer cancel()
  70. req = req.WithContext(ctx)
  71. allResponses, err := httpu.Do(req, numSends)
  72. if err != nil {
  73. return nil, err
  74. }
  75. isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
  76. seenUSNs := make(map[string]bool)
  77. var responses []*http.Response
  78. for _, response := range allResponses {
  79. if response.StatusCode != 200 {
  80. log.Printf("ssdp: got response status code %q in search response", response.Status)
  81. continue
  82. }
  83. if st := response.Header.Get("ST"); isExactSearch && st != searchTarget {
  84. continue
  85. }
  86. usn := response.Header.Get("USN")
  87. if usn == "" {
  88. // Empty/missing USN in search response - using location instead.
  89. location, err := response.Location()
  90. if err != nil {
  91. // No usable location in search response - discard.
  92. continue
  93. }
  94. usn = location.String()
  95. }
  96. if _, alreadySeen := seenUSNs[usn]; !alreadySeen {
  97. seenUSNs[usn] = true
  98. responses = append(responses, response)
  99. }
  100. }
  101. return responses, nil
  102. }