goupnp.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // goupnp is an implementation of a client for various UPnP services.
  2. //
  3. // For most uses, it is recommended to use the code-generated packages under
  4. // github.com/tailscale/goupnp/dcps. Example use is shown at
  5. // http://godoc.org/github.com/tailscale/goupnp/example
  6. //
  7. // A commonly used client is internetgateway1.WANPPPConnection1:
  8. // http://godoc.org/github.com/tailscale/goupnp/dcps/internetgateway1#WANPPPConnection1
  9. //
  10. // Currently only a couple of schemas have code generated for them from the
  11. // UPnP example XML specifications. Not all methods will work on these clients,
  12. // because the generated stubs contain the full set of specified methods from
  13. // the XML specifications, and the discovered services will likely support a
  14. // subset of those methods.
  15. package goupnp
  16. import (
  17. "context"
  18. "encoding/xml"
  19. "fmt"
  20. "io"
  21. "net/http"
  22. "net/url"
  23. "github.com/tailscale/goupnp/ssdp"
  24. )
  25. // ContextError is an error that wraps an error with some context information.
  26. type ContextError struct {
  27. Context string
  28. Err error
  29. }
  30. func ctxError(err error, msg string) ContextError {
  31. return ContextError{Context: msg, Err: err}
  32. }
  33. func ctxErrorf(err error, msg string, args ...interface{}) ContextError {
  34. return ContextError{
  35. Context: fmt.Sprintf(msg, args...),
  36. Err: err,
  37. }
  38. }
  39. func (err ContextError) Error() string {
  40. return fmt.Sprintf("%s: %v", err.Context, err.Err)
  41. }
  42. // MaybeRootDevice contains either a RootDevice or an error.
  43. type MaybeRootDevice struct {
  44. // Identifier of the device.
  45. USN string
  46. // Set iff Err == nil.
  47. Root *RootDevice
  48. // The location the device was discovered at. This can be used with
  49. // DeviceByURL, assuming the device is still present. A location represents
  50. // the discovery of a device, regardless of if there was an error probing it.
  51. Location *url.URL
  52. // Any error encountered probing a discovered device.
  53. Err error
  54. }
  55. // DiscoverDevices attempts to find targets of the given type. This is
  56. // typically the entry-point for this package. searchTarget is typically a URN
  57. // in the form "urn:schemas-upnp-org:device:..." or
  58. // "urn:schemas-upnp-org:service:...". A single error is returned for errors
  59. // while attempting to send the query. An error or RootDevice is returned for
  60. // each discovered RootDevice.
  61. func DiscoverDevices(ctx context.Context, searchTarget string) ([]MaybeRootDevice, error) {
  62. hc, err := httpuClient()
  63. if err != nil {
  64. return nil, err
  65. }
  66. defer hc.Close()
  67. responses, err := ssdp.SSDPRawSearch(ctx, hc, string(searchTarget), 3)
  68. if err != nil {
  69. return nil, err
  70. }
  71. results := make([]MaybeRootDevice, len(responses))
  72. for i, response := range responses {
  73. maybe := &results[i]
  74. maybe.USN = response.Header.Get("USN")
  75. loc, err := response.Location()
  76. if err != nil {
  77. maybe.Err = ContextError{"unexpected bad location from search", err}
  78. continue
  79. }
  80. maybe.Location = loc
  81. if root, err := DeviceByURL(ctx, loc); err != nil {
  82. maybe.Err = err
  83. } else {
  84. maybe.Root = root
  85. }
  86. }
  87. return results, nil
  88. }
  89. func DeviceByURL(ctx context.Context, loc *url.URL) (*RootDevice, error) {
  90. locStr := loc.String()
  91. root := new(RootDevice)
  92. if err := requestXml(ctx, locStr, DeviceXMLNamespace, root); err != nil {
  93. return nil, ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
  94. }
  95. var urlBaseStr string
  96. if root.URLBaseStr != "" {
  97. urlBaseStr = root.URLBaseStr
  98. } else {
  99. urlBaseStr = locStr
  100. }
  101. urlBase, err := url.Parse(urlBaseStr)
  102. if err != nil {
  103. return nil, ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
  104. }
  105. root.SetURLBase(urlBase)
  106. return root, nil
  107. }
  108. // CharsetReaderDefault specifies the charset reader used while decoding the output
  109. // from a UPnP server. It can be modified in an init function to allow for non-utf8 encodings,
  110. // but should not be changed after requesting clients.
  111. var CharsetReaderDefault func(charset string, input io.Reader) (io.Reader, error)
  112. // contextKey is an unexported type which prevents construction of other contextKeys.
  113. type httpContextKey struct{}
  114. // WithHTTPClient returns a context wrapping ctx with the HTTP client set to c.
  115. // If c is nil, http.DefaultClient is used.
  116. func WithHTTPClient(ctx context.Context, c *http.Client) context.Context {
  117. return context.WithValue(ctx, httpContextKey{}, c)
  118. }
  119. func httpClient(ctx context.Context) *http.Client {
  120. if c, _ := ctx.Value(httpContextKey{}).(*http.Client); c != nil {
  121. return c
  122. }
  123. return http.DefaultClient
  124. }
  125. func requestXml(ctx context.Context, url string, defaultSpace string, into interface{}) error {
  126. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  127. if err != nil {
  128. return err
  129. }
  130. resp, err := httpClient(ctx).Do(req)
  131. if err != nil {
  132. return err
  133. }
  134. defer resp.Body.Close()
  135. if resp.StatusCode != 200 {
  136. return fmt.Errorf("goupnp: got response status %s from %q", resp.Status, url)
  137. }
  138. decoder := xml.NewDecoder(resp.Body)
  139. decoder.DefaultSpace = defaultSpace
  140. decoder.CharsetReader = CharsetReaderDefault
  141. return decoder.Decode(into)
  142. }