Commit bf11a47f authored by Felix Lange's avatar Felix Lange

Godeps: upgrade github.com/huin/goupnp to 90f71cb5

parent fc46cf33
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
}, },
{ {
"ImportPath": "github.com/huin/goupnp", "ImportPath": "github.com/huin/goupnp",
"Rev": "5cff77a69fb22f5f1774c4451ea2aab63d4d2f20" "Rev": "90f71cb5dd6d4606388666d2cda4ce2f563d2185"
}, },
{ {
"ImportPath": "github.com/jackpal/go-nat-pmp", "ImportPath": "github.com/jackpal/go-nat-pmp",
......
...@@ -5,10 +5,40 @@ Installation ...@@ -5,10 +5,40 @@ Installation
Run `go get -u github.com/huin/goupnp`. Run `go get -u github.com/huin/goupnp`.
Documentation
-------------
All doc links below are for ![GoDoc](https://godoc.org/github.com/huin/goupnp?status.svg).
Supported DCPs (you probably want to start with one of these):
* [av1](https://godoc.org/github.com/huin/goupnp/dcps/av1) - Client for UPnP Device Control Protocol MediaServer v1 and MediaRenderer v1.
* [internetgateway1](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway1) - Client for UPnP Device Control Protocol Internet Gateway Device v1.
* [internetgateway2](https://godoc.org/github.com/huin/goupnp/dcps/internetgateway2) - Client for UPnP Device Control Protocol Internet Gateway Device v2.
Core components:
* [(goupnp)](https://godoc.org/github.com/huin/goupnp) core library - contains datastructures and utilities typically used by the implemented DCPs.
* [httpu](https://godoc.org/github.com/huin/goupnp/httpu) HTTPU implementation, underlies SSDP.
* [ssdp](https://godoc.org/github.com/huin/goupnp/ssdp) SSDP client implementation (simple service discovery protocol) - used to discover UPnP services on a network.
* [soap](https://godoc.org/github.com/huin/goupnp/soap) SOAP client implementation (simple object access protocol) - used to communicate with discovered services.
Regenerating dcps generated source code: Regenerating dcps generated source code:
---------------------------------------- ----------------------------------------
1. Install gotasks: `go get -u github.com/jingweno/gotask` 1. Install gotasks: `go get -u github.com/jingweno/gotask`
2. Change to the gotasks directory: `cd gotasks` 2. Change to the gotasks directory: `cd gotasks`
3. Download UPnP specification data (if not done already): `wget http://upnp.org/resources/upnpresources.zip` 3. Run specgen task: `gotask specgen`
4. Regenerate source code: `gotask specgen -s upnpresources.zip -o ../dcps`
Supporting additional UPnP devices and services:
------------------------------------------------
Supporting additional services is, in the trivial case, simply a matter of
adding the service to the `dcpMetadata` whitelist in `gotasks/specgen_task.go`,
regenerating the source code (see above), and committing that source code.
However, it would be helpful if anyone needing such a service could test the
service against the service they have, and then reporting any trouble
encountered as an [issue on this
project](https://github.com/huin/goupnp/issues/new). If it just works, then
please report at least minimal working functionality as an issue, and
optionally contribute the metadata upstream.
package main
import (
"log"
"github.com/huin/goupnp/ssdp"
)
func main() {
c := make(chan ssdp.Update)
srv, reg := ssdp.NewServerAndRegistry()
reg.AddListener(c)
go listener(c)
if err := srv.ListenAndServe(); err != nil {
log.Print("ListenAndServe failed: ", err)
}
}
func listener(c <-chan ssdp.Update) {
for u := range c {
if u.Entry != nil {
log.Printf("Event: %v USN: %s Entry: %#v", u.EventType, u.USN, *u.Entry)
} else {
log.Printf("Event: %v USN: %s Entry: <nil>", u.EventType, u.USN)
}
}
}
This diff is collapsed.
package example_test
import (
"fmt"
"os"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
)
// Use discovered WANPPPConnection1 services to find external IP addresses.
func Example_WANPPPConnection1_GetExternalIPAddress() {
clients, errors, err := internetgateway1.NewWANPPPConnection1Clients()
extIPClients := make([]GetExternalIPAddresser, len(clients))
for i, client := range clients {
extIPClients[i] = client
}
DisplayExternalIPResults(extIPClients, errors, err)
// Output:
}
// Use discovered WANIPConnection services to find external IP addresses.
func Example_WANIPConnection_GetExternalIPAddress() {
clients, errors, err := internetgateway1.NewWANIPConnection1Clients()
extIPClients := make([]GetExternalIPAddresser, len(clients))
for i, client := range clients {
extIPClients[i] = client
}
DisplayExternalIPResults(extIPClients, errors, err)
// Output:
}
type GetExternalIPAddresser interface {
GetExternalIPAddress() (NewExternalIPAddress string, err error)
GetServiceClient() *goupnp.ServiceClient
}
func DisplayExternalIPResults(clients []GetExternalIPAddresser, errors []error, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "Error discovering service with UPnP: ", err)
return
}
if len(errors) > 0 {
fmt.Fprintf(os.Stderr, "Error discovering %d services:\n", len(errors))
for _, err := range errors {
fmt.Println(" ", err)
}
}
fmt.Fprintf(os.Stderr, "Successfully discovered %d services:\n", len(clients))
for _, client := range clients {
device := &client.GetServiceClient().RootDevice.Device
fmt.Fprintln(os.Stderr, " Device:", device.FriendlyName)
if addr, err := client.GetExternalIPAddress(); err != nil {
fmt.Fprintf(os.Stderr, " Failed to get external IP address: %v\n", err)
} else {
fmt.Fprintf(os.Stderr, " External IP address: %v\n", addr)
}
}
}
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
"golang.org/x/net/html/charset" "golang.org/x/net/html/charset"
"github.com/huin/goupnp/httpu" "github.com/huin/goupnp/httpu"
...@@ -38,7 +39,15 @@ func (err ContextError) Error() string { ...@@ -38,7 +39,15 @@ func (err ContextError) Error() string {
// MaybeRootDevice contains either a RootDevice or an error. // MaybeRootDevice contains either a RootDevice or an error.
type MaybeRootDevice struct { type MaybeRootDevice struct {
// Set iff Err == nil.
Root *RootDevice 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 Err error
} }
...@@ -67,11 +76,22 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) { ...@@ -67,11 +76,22 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
maybe.Err = ContextError{"unexpected bad location from search", err} maybe.Err = ContextError{"unexpected bad location from search", err}
continue 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() locStr := loc.String()
root := new(RootDevice) root := new(RootDevice)
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil { if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil {
maybe.Err = ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err} return nil, ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
continue
} }
var urlBaseStr string var urlBaseStr string
if root.URLBaseStr != "" { if root.URLBaseStr != "" {
...@@ -81,14 +101,10 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) { ...@@ -81,14 +101,10 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
} }
urlBase, err := url.Parse(urlBaseStr) urlBase, err := url.Parse(urlBaseStr)
if err != nil { if err != nil {
maybe.Err = ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err} return nil, ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
continue
} }
root.SetURLBase(urlBase) root.SetURLBase(urlBase)
maybe.Root = root return root, nil
}
return results, nil
} }
func requestXml(url string, defaultSpace string, doc interface{}) error { func requestXml(url string, defaultSpace string, doc interface{}) error {
......
...@@ -2,18 +2,26 @@ package goupnp ...@@ -2,18 +2,26 @@ package goupnp
import ( import (
"fmt" "fmt"
"net/url"
"github.com/huin/goupnp/soap" "github.com/huin/goupnp/soap"
) )
// ServiceClient is a SOAP client, root device and the service for the SOAP // ServiceClient is a SOAP client, root device and the service for the SOAP
// client rolled into one value. The root device and service are intended to be // client rolled into one value. The root device, location, and service are
// informational. // intended to be informational. Location can be used to later recreate a
// ServiceClient with NewServiceClientByURL if the service is still present;
// bypassing the discovery process.
type ServiceClient struct { type ServiceClient struct {
SOAPClient *soap.SOAPClient SOAPClient *soap.SOAPClient
RootDevice *RootDevice RootDevice *RootDevice
Location *url.URL
Service *Service Service *Service
} }
// NewServiceClients discovers services, and returns clients for them. err will
// report any error with the discovery process (blocking any device/service
// discovery), errors reports errors on a per-root-device basis.
func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []error, err error) { func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []error, err error) {
var maybeRootDevices []MaybeRootDevice var maybeRootDevices []MaybeRootDevice
if maybeRootDevices, err = DiscoverDevices(searchTarget); err != nil { if maybeRootDevices, err = DiscoverDevices(searchTarget); err != nil {
...@@ -28,24 +36,48 @@ func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []e ...@@ -28,24 +36,48 @@ func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []e
continue continue
} }
device := &maybeRootDevice.Root.Device deviceClients, err := NewServiceClientsFromRootDevice(maybeRootDevice.Root, maybeRootDevice.Location, searchTarget)
if err != nil {
errors = append(errors, err)
continue
}
clients = append(clients, deviceClients...)
}
return
}
// NewServiceClientsByURL creates client(s) for the given service URN, for a
// root device at the given URL.
func NewServiceClientsByURL(loc *url.URL, searchTarget string) ([]ServiceClient, error) {
rootDevice, err := DeviceByURL(loc)
if err != nil {
return nil, err
}
return NewServiceClientsFromRootDevice(rootDevice, loc, searchTarget)
}
// NewServiceClientsFromDevice creates client(s) for the given service URN, in
// a given root device. The loc parameter is simply assigned to the
// Location attribute of the returned ServiceClient(s).
func NewServiceClientsFromRootDevice(rootDevice *RootDevice, loc *url.URL, searchTarget string) ([]ServiceClient, error) {
device := &rootDevice.Device
srvs := device.FindService(searchTarget) srvs := device.FindService(searchTarget)
if len(srvs) == 0 { if len(srvs) == 0 {
errors = append(errors, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)", return nil, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
searchTarget, device.FriendlyName, device.UDN)) searchTarget, device.FriendlyName, device.UDN)
continue
} }
clients := make([]ServiceClient, 0, len(srvs))
for _, srv := range srvs { for _, srv := range srvs {
clients = append(clients, ServiceClient{ clients = append(clients, ServiceClient{
SOAPClient: srv.NewSOAPClient(), SOAPClient: srv.NewSOAPClient(),
RootDevice: maybeRootDevice.Root, RootDevice: rootDevice,
Location: loc,
Service: srv, Service: srv,
}) })
} }
} return clients, nil
return
} }
// GetServiceClient returns the ServiceClient itself. This is provided so that the // GetServiceClient returns the ServiceClient itself. This is provided so that the
......
package soap
import (
"bytes"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"testing"
)
type capturingRoundTripper struct {
err error
resp *http.Response
capturedReq *http.Request
}
func (rt *capturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
rt.capturedReq = req
return rt.resp, rt.err
}
func TestActionInputs(t *testing.T) {
url, err := url.Parse("http://example.com/soap")
if err != nil {
t.Fatal(err)
}
rt := &capturingRoundTripper{
err: nil,
resp: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:myactionResponse xmlns:u="mynamespace">
<A>valueA</A>
<B>valueB</B>
</u:myactionResponse>
</s:Body>
</s:Envelope>
`)),
},
}
client := SOAPClient{
EndpointURL: *url,
HTTPClient: http.Client{
Transport: rt,
},
}
type In struct {
Foo string
Bar string `soap:"bar"`
}
type Out struct {
A string
B string
}
in := In{"foo", "bar"}
gotOut := Out{}
err = client.PerformAction("mynamespace", "myaction", &in, &gotOut)
if err != nil {
t.Fatal(err)
}
wantBody := (soapPrefix +
`<u:myaction xmlns:u="mynamespace">` +
`<Foo>foo</Foo>` +
`<bar>bar</bar>` +
`</u:myaction>` +
soapSuffix)
body, err := ioutil.ReadAll(rt.capturedReq.Body)
if err != nil {
t.Fatal(err)
}
gotBody := string(body)
if wantBody != gotBody {
t.Errorf("Bad request body\nwant: %q\n got: %q", wantBody, gotBody)
}
wantOut := Out{"valueA", "valueB"}
if !reflect.DeepEqual(wantOut, gotOut) {
t.Errorf("Bad output\nwant: %+v\n got: %+v", wantOut, gotOut)
}
}
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
...@@ -506,3 +507,13 @@ func MarshalBinHex(v []byte) (string, error) { ...@@ -506,3 +507,13 @@ func MarshalBinHex(v []byte) (string, error) {
func UnmarshalBinHex(s string) ([]byte, error) { func UnmarshalBinHex(s string) ([]byte, error) {
return hex.DecodeString(s) return hex.DecodeString(s)
} }
// MarshalURI marshals *url.URL to SOAP "uri" type.
func MarshalURI(v *url.URL) (string, error) {
return v.String(), nil
}
// UnmarshalURI unmarshals *url.URL from the SOAP "uri" type.
func UnmarshalURI(s string) (*url.URL, error) {
return url.Parse(s)
}
...@@ -21,6 +21,40 @@ var ( ...@@ -21,6 +21,40 @@ var (
maxAgeRx = regexp.MustCompile("max-age=([0-9]+)") maxAgeRx = regexp.MustCompile("max-age=([0-9]+)")
) )
const (
EventAlive = EventType(iota)
EventUpdate
EventByeBye
)
type EventType int8
func (et EventType) String() string {
switch et {
case EventAlive:
return "EventAlive"
case EventUpdate:
return "EventUpdate"
case EventByeBye:
return "EventByeBye"
default:
return fmt.Sprintf("EventUnknown(%d)", int8(et))
}
}
type Update struct {
// The USN of the service.
USN string
// What happened.
EventType EventType
// The entry, which is nil if the service was not known and
// EventType==EventByeBye. The contents of this must not be modified as it is
// shared with the registry and other listeners. Once created, the Registry
// does not modify the Entry value - any updates are replaced with a new
// Entry value.
Entry *Entry
}
type Entry struct { type Entry struct {
// The address that the entry data was actually received from. // The address that the entry data was actually received from.
RemoteAddr string RemoteAddr string
...@@ -32,7 +66,7 @@ type Entry struct { ...@@ -32,7 +66,7 @@ type Entry struct {
Server string Server string
Host string Host string
// Location of the UPnP root device description. // Location of the UPnP root device description.
Location *url.URL Location url.URL
// Despite BOOTID,CONFIGID being required fields, apparently they are not // Despite BOOTID,CONFIGID being required fields, apparently they are not
// always set by devices. Set to -1 if not present. // always set by devices. Set to -1 if not present.
...@@ -83,7 +117,7 @@ func newEntryFromRequest(r *http.Request) (*Entry, error) { ...@@ -83,7 +117,7 @@ func newEntryFromRequest(r *http.Request) (*Entry, error) {
NT: r.Header.Get("NT"), NT: r.Header.Get("NT"),
Server: r.Header.Get("SERVER"), Server: r.Header.Get("SERVER"),
Host: r.Header.Get("HOST"), Host: r.Header.Get("HOST"),
Location: loc, Location: *loc,
BootID: bootID, BootID: bootID,
ConfigID: configID, ConfigID: configID,
SearchPort: uint16(searchPort), SearchPort: uint16(searchPort),
...@@ -125,15 +159,71 @@ func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int3 ...@@ -125,15 +159,71 @@ func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int3
var _ httpu.Handler = new(Registry) var _ httpu.Handler = new(Registry)
// Registry maintains knowledge of discovered devices and services. // Registry maintains knowledge of discovered devices and services.
//
// NOTE: the interface for this is experimental and may change, or go away
// entirely.
type Registry struct { type Registry struct {
lock sync.Mutex lock sync.Mutex
byUSN map[string]*Entry byUSN map[string]*Entry
listenersLock sync.RWMutex
listeners map[chan<- Update]struct{}
} }
func NewRegistry() *Registry { func NewRegistry() *Registry {
return &Registry{ return &Registry{
byUSN: make(map[string]*Entry), byUSN: make(map[string]*Entry),
listeners: make(map[chan<- Update]struct{}),
}
}
// NewServerAndRegistry is a convenience function to create a registry, and an
// httpu server to pass it messages. Call ListenAndServe on the server for
// messages to be processed.
func NewServerAndRegistry() (*httpu.Server, *Registry) {
reg := NewRegistry()
srv := &httpu.Server{
Addr: ssdpUDP4Addr,
Multicast: true,
Handler: reg,
} }
return srv, reg
}
func (reg *Registry) AddListener(c chan<- Update) {
reg.listenersLock.Lock()
defer reg.listenersLock.Unlock()
reg.listeners[c] = struct{}{}
}
func (reg *Registry) RemoveListener(c chan<- Update) {
reg.listenersLock.Lock()
defer reg.listenersLock.Unlock()
delete(reg.listeners, c)
}
func (reg *Registry) sendUpdate(u Update) {
reg.listenersLock.RLock()
defer reg.listenersLock.RUnlock()
for c := range reg.listeners {
c <- u
}
}
// GetService returns known service (or device) entries for the given service
// URN.
func (reg *Registry) GetService(serviceURN string) []*Entry {
// Currently assumes that the map is small, so we do a linear search rather
// than indexed to avoid maintaining two maps.
var results []*Entry
reg.lock.Lock()
defer reg.lock.Unlock()
for _, entry := range reg.byUSN {
if entry.NT == serviceURN {
results = append(results, entry)
}
}
return results
} }
// ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to // ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to
...@@ -156,7 +246,9 @@ func (reg *Registry) ServeMessage(r *http.Request) { ...@@ -156,7 +246,9 @@ func (reg *Registry) ServeMessage(r *http.Request) {
default: default:
err = fmt.Errorf("unknown NTS value: %q", nts) err = fmt.Errorf("unknown NTS value: %q", nts)
} }
log.Printf("In %s request from %s: %v", nts, r.RemoteAddr, err) if err != nil {
log.Printf("goupnp/ssdp: failed to handle %s message from %s: %v", nts, r.RemoteAddr, err)
}
} }
func (reg *Registry) handleNTSAlive(r *http.Request) error { func (reg *Registry) handleNTSAlive(r *http.Request) error {
...@@ -166,9 +258,14 @@ func (reg *Registry) handleNTSAlive(r *http.Request) error { ...@@ -166,9 +258,14 @@ func (reg *Registry) handleNTSAlive(r *http.Request) error {
} }
reg.lock.Lock() reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventAlive,
Entry: entry,
})
return nil return nil
} }
...@@ -185,18 +282,31 @@ func (reg *Registry) handleNTSUpdate(r *http.Request) error { ...@@ -185,18 +282,31 @@ func (reg *Registry) handleNTSUpdate(r *http.Request) error {
entry.BootID = nextBootID entry.BootID = nextBootID
reg.lock.Lock() reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventUpdate,
Entry: entry,
})
return nil return nil
} }
func (reg *Registry) handleNTSByebye(r *http.Request) error { func (reg *Registry) handleNTSByebye(r *http.Request) error {
reg.lock.Lock() usn := r.Header.Get("USN")
defer reg.lock.Unlock()
delete(reg.byUSN, r.Header.Get("USN")) reg.lock.Lock()
entry := reg.byUSN[usn]
delete(reg.byUSN, usn)
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: usn,
EventType: EventByeBye,
Entry: entry,
})
return nil return nil
} }
...@@ -148,7 +148,12 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, ...@@ -148,7 +148,12 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
return return
} }
// check for a matching IGD service // check for a matching IGD service
sc := goupnp.ServiceClient{service.NewSOAPClient(), devs[i].Root, service} sc := goupnp.ServiceClient{
SOAPClient: service.NewSOAPClient(),
RootDevice: devs[i].Root,
Location: devs[i].Location,
Service: service,
}
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
upnp := matcher(devs[i].Root, sc) upnp := matcher(devs[i].Root, sc)
if upnp == nil { if upnp == nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment