-
Notifications
You must be signed in to change notification settings - Fork 85
/
goupnp.go
183 lines (157 loc) · 5.49 KB
/
goupnp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// goupnp is an implementation of a client for various UPnP services.
//
// For most uses, it is recommended to use the code-generated packages under
// github.com/huin/goupnp/dcps. Example use is shown at
// http://godoc.org/github.com/huin/goupnp/example
//
// A commonly used client is internetgateway1.WANPPPConnection1:
// http://godoc.org/github.com/huin/goupnp/dcps/internetgateway1#WANPPPConnection1
//
// Currently only a couple of schemas have code generated for them from the
// UPnP example XML specifications. Not all methods will work on these clients,
// because the generated stubs contain the full set of specified methods from
// the XML specifications, and the discovered services will likely support a
// subset of those methods.
package goupnp
import (
"context"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/huin/goupnp/httpu"
"github.com/huin/goupnp/ssdp"
)
// ContextError is an error that wraps an error with some context information.
type ContextError struct {
Context string
Err error
}
func ctxError(err error, msg string) ContextError {
return ContextError{
Context: msg,
Err: err,
}
}
func ctxErrorf(err error, msg string, args ...interface{}) ContextError {
return ContextError{
Context: fmt.Sprintf(msg, args...),
Err: err,
}
}
func (err ContextError) Error() string {
return fmt.Sprintf("%s: %v", err.Context, err.Err)
}
// MaybeRootDevice contains either a RootDevice or an error.
type MaybeRootDevice struct {
// Identifier of the device. Note that this in combination with Location
// uniquely identifies a result from DiscoverDevices.
USN string
// Set iff Err == nil.
Root *RootDevice
// The location the device was discovered at. This can be used with
// DeviceByURL, assuming the device is still present. A location represents
// the discovery of a device, regardless of if there was an error probing it.
Location *url.URL
// The address from which the device was discovered (if known - otherwise nil).
LocalAddr net.IP
// Any error encountered probing a discovered device.
Err error
}
// DiscoverDevicesCtx attempts to find targets of the given type. This is
// typically the entry-point for this package. searchTarget is typically a URN
// in the form "urn:schemas-upnp-org:device:..." or
// "urn:schemas-upnp-org:service:...". A single error is returned for errors
// while attempting to send the query. An error or RootDevice is returned for
// each discovered RootDevice.
func DiscoverDevicesCtx(ctx context.Context, searchTarget string) ([]MaybeRootDevice, error) {
hc, hcCleanup, err := httpuClient()
if err != nil {
return nil, err
}
defer hcCleanup()
searchCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
responses, err := ssdp.RawSearch(searchCtx, hc, string(searchTarget), 3)
if err != nil {
return nil, err
}
results := make([]MaybeRootDevice, len(responses))
for i, response := range responses {
maybe := &results[i]
maybe.USN = response.Header.Get("USN")
loc, err := response.Location()
if err != nil {
maybe.Err = ContextError{"unexpected bad location from search", err}
continue
}
maybe.Location = loc
if root, err := DeviceByURLCtx(ctx, loc); err != nil {
maybe.Err = err
} else {
maybe.Root = root
}
if i := response.Header.Get(httpu.LocalAddressHeader); len(i) > 0 {
maybe.LocalAddr = net.ParseIP(i)
}
}
return results, nil
}
// DiscoverDevices is the legacy version of DiscoverDevicesCtx, but uses
// context.Background() as the context.
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
return DiscoverDevicesCtx(context.Background(), searchTarget)
}
func DeviceByURLCtx(ctx context.Context, loc *url.URL) (*RootDevice, error) {
locStr := loc.String()
root := new(RootDevice)
if err := requestXml(ctx, locStr, DeviceXMLNamespace, root); err != nil {
return nil, ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
}
var urlBaseStr string
if root.URLBaseStr != "" {
urlBaseStr = root.URLBaseStr
} else {
urlBaseStr = locStr
}
urlBase, err := url.Parse(urlBaseStr)
if err != nil {
return nil, ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
}
root.SetURLBase(urlBase)
return root, nil
}
func DeviceByURL(loc *url.URL) (*RootDevice, error) {
return DeviceByURLCtx(context.Background(), loc)
}
// CharsetReaderDefault specifies the charset reader used while decoding the output
// from a UPnP server. It can be modified in an init function to allow for non-utf8 encodings,
// but should not be changed after requesting clients.
var CharsetReaderDefault func(charset string, input io.Reader) (io.Reader, error)
// HTTPClient specifies the http.Client object used when fetching the XML from the UPnP server.
// HTTPClient defaults the http.DefaultClient. This may be overridden by the importing application.
var HTTPClientDefault = http.DefaultClient
func requestXml(ctx context.Context, url string, defaultSpace string, doc interface{}) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := HTTPClientDefault.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("goupnp: got response status %s from %q",
resp.Status, url)
}
decoder := xml.NewDecoder(resp.Body)
decoder.DefaultSpace = defaultSpace
decoder.CharsetReader = CharsetReaderDefault
return decoder.Decode(doc)
}