aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/huin/goupnp/goupnp.go
blob: fcb8dcd23ded572dd1519dde1752f1f74a40235a (plain) (blame)
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
// 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 (
    "encoding/xml"
    "fmt"
    "net/http"
    "net/url"
    "time"

    "golang.org/x/net/html/charset"

    "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 (err ContextError) Error() string {
    return fmt.Sprintf("%s: %v", err.Context, err.Err)
}

// MaybeRootDevice contains either a RootDevice or an error.
type MaybeRootDevice struct {
    // 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

    // Any error encountered probing a discovered device.
    Err error
}

// DiscoverDevices 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 DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
    httpu, err := httpu.NewHTTPUClient()
    if err != nil {
        return nil, err
    }
    defer httpu.Close()
    responses, err := ssdp.SSDPRawSearch(httpu, string(searchTarget), 2, 3)
    if err != nil {
        return nil, err
    }

    results := make([]MaybeRootDevice, len(responses))
    for i, response := range responses {
        maybe := &results[i]
        loc, err := response.Location()
        if err != nil {
            maybe.Err = ContextError{"unexpected bad location from search", err}
            continue
        }
        maybe.Location = loc
        if root, err := DeviceByURL(loc); err != nil {
            maybe.Err = err
        } else {
            maybe.Root = root
        }
    }

    return results, nil
}

func DeviceByURL(loc *url.URL) (*RootDevice, error) {
    locStr := loc.String()
    root := new(RootDevice)
    if err := requestXml(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 requestXml(url string, defaultSpace string, doc interface{}) error {
    timeout := time.Duration(3 * time.Second)
    client := http.Client{
        Timeout: timeout,
    }
    resp, err := client.Get(url)
    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 = charset.NewReaderLabel

    return decoder.Decode(doc)
}