Commit bf11a47f authored by Felix Lange's avatar Felix Lange

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

parent fc46cf33
......@@ -34,7 +34,7 @@
},
{
"ImportPath": "github.com/huin/goupnp",
"Rev": "5cff77a69fb22f5f1774c4451ea2aab63d4d2f20"
"Rev": "90f71cb5dd6d4606388666d2cda4ce2f563d2185"
},
{
"ImportPath": "github.com/jackpal/go-nat-pmp",
......
......@@ -5,10 +5,40 @@ Installation
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:
----------------------------------------
1. Install gotasks: `go get -u github.com/jingweno/gotask`
2. Change to the gotasks directory: `cd gotasks`
3. Download UPnP specification data (if not done already): `wget http://upnp.org/resources/upnpresources.zip`
4. Regenerate source code: `gotask specgen -s upnpresources.zip -o ../dcps`
3. Run specgen task: `gotask specgen`
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 (
"net/http"
"net/url"
"time"
"golang.org/x/net/html/charset"
"github.com/huin/goupnp/httpu"
......@@ -38,8 +39,16 @@ func (err ContextError) Error() string {
// MaybeRootDevice contains either a RootDevice or an error.
type MaybeRootDevice struct {
// Set iff Err == nil.
Root *RootDevice
Err error
// 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
......@@ -67,30 +76,37 @@ func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
maybe.Err = ContextError{"unexpected bad location from search", err}
continue
}
locStr := loc.String()
root := new(RootDevice)
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil {
maybe.Err = ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
continue
}
var urlBaseStr string
if root.URLBaseStr != "" {
urlBaseStr = root.URLBaseStr
maybe.Location = loc
if root, err := DeviceByURL(loc); err != nil {
maybe.Err = err
} else {
urlBaseStr = locStr
}
urlBase, err := url.Parse(urlBaseStr)
if err != nil {
maybe.Err = ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
continue
maybe.Root = root
}
root.SetURLBase(urlBase)
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{
......
......@@ -2,18 +2,26 @@ package goupnp
import (
"fmt"
"net/url"
"github.com/huin/goupnp/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
// informational.
// client rolled into one value. The root device, location, and service are
// 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 {
SOAPClient *soap.SOAPClient
RootDevice *RootDevice
Location *url.URL
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) {
var maybeRootDevices []MaybeRootDevice
if maybeRootDevices, err = DiscoverDevices(searchTarget); err != nil {
......@@ -28,26 +36,50 @@ func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []e
continue
}
device := &maybeRootDevice.Root.Device
srvs := device.FindService(searchTarget)
if len(srvs) == 0 {
errors = append(errors, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
searchTarget, device.FriendlyName, device.UDN))
deviceClients, err := NewServiceClientsFromRootDevice(maybeRootDevice.Root, maybeRootDevice.Location, searchTarget)
if err != nil {
errors = append(errors, err)
continue
}
for _, srv := range srvs {
clients = append(clients, ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: maybeRootDevice.Root,
Service: srv,
})
}
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)
if len(srvs) == 0 {
return nil, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
searchTarget, device.FriendlyName, device.UDN)
}
clients := make([]ServiceClient, 0, len(srvs))
for _, srv := range srvs {
clients = append(clients, ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: rootDevice,
Location: loc,
Service: srv,
})
}
return clients, nil
}
// GetServiceClient returns the ServiceClient itself. This is provided so that the
// service client attributes can be accessed via an interface method on a
// wrapping type.
......
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 (
"encoding/hex"
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
......@@ -506,3 +507,13 @@ func MarshalBinHex(v []byte) (string, error) {
func UnmarshalBinHex(s string) ([]byte, error) {
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 (
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 {
// The address that the entry data was actually received from.
RemoteAddr string
......@@ -32,7 +66,7 @@ type Entry struct {
Server string
Host string
// Location of the UPnP root device description.
Location *url.URL
Location url.URL
// Despite BOOTID,CONFIGID being required fields, apparently they are not
// always set by devices. Set to -1 if not present.
......@@ -83,7 +117,7 @@ func newEntryFromRequest(r *http.Request) (*Entry, error) {
NT: r.Header.Get("NT"),
Server: r.Header.Get("SERVER"),
Host: r.Header.Get("HOST"),
Location: loc,
Location: *loc,
BootID: bootID,
ConfigID: configID,
SearchPort: uint16(searchPort),
......@@ -125,15 +159,71 @@ func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int3
var _ httpu.Handler = new(Registry)
// 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 {
lock sync.Mutex
byUSN map[string]*Entry
listenersLock sync.RWMutex
listeners map[chan<- Update]struct{}
}
func NewRegistry() *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
......@@ -156,7 +246,9 @@ func (reg *Registry) ServeMessage(r *http.Request) {
default:
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 {
......@@ -166,9 +258,14 @@ func (reg *Registry) handleNTSAlive(r *http.Request) error {
}
reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventAlive,
Entry: entry,
})
return nil
}
......@@ -185,18 +282,31 @@ func (reg *Registry) handleNTSUpdate(r *http.Request) error {
entry.BootID = nextBootID
reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry
reg.lock.Unlock()
reg.sendUpdate(Update{
USN: entry.USN,
EventType: EventUpdate,
Entry: entry,
})
return nil
}
func (reg *Registry) handleNTSByebye(r *http.Request) error {
reg.lock.Lock()
defer reg.lock.Unlock()
usn := r.Header.Get("USN")
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
}
......@@ -148,7 +148,12 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
return
}
// 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
upnp := matcher(devs[i].Root, sc)
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