Merge pull request #1951 from fjl/godeps-upgrade-goupnp

Godeps: upgrade github.com/huin/goupnp
This commit is contained in:
Jeffrey Wilcke 2015-10-30 00:14:15 +01:00
commit 1abbe05e93
16 changed files with 10236 additions and 2246 deletions

2
Godeps/Godeps.json generated
View File

@ -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",

View File

@ -0,0 +1 @@
/gotasks/specs

View File

@ -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.

View File

@ -0,0 +1,27 @@
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)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +0,0 @@
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)
}
}
}

View File

@ -4,12 +4,11 @@ package gotasks
import ( import (
"archive/zip" "archive/zip"
"bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -28,6 +27,53 @@ var (
serviceURNPrefix = "urn:schemas-upnp-org:service:" serviceURNPrefix = "urn:schemas-upnp-org:service:"
) )
// DCP contains extra metadata to use when generating DCP source files.
type DCPMetadata struct {
Name string // What to name the Go DCP package.
OfficialName string // Official name for the DCP.
DocURL string // Optional - URL for futher documentation about the DCP.
XMLSpecURL string // Where to download the XML spec from.
// Any special-case functions to run against the DCP before writing it out.
Hacks []DCPHackFn
}
var dcpMetadata = []DCPMetadata{
{
Name: "internetgateway1",
OfficialName: "Internet Gateway Device v1",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
},
{
Name: "internetgateway2",
OfficialName: "Internet Gateway Device v2",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
Hacks: []DCPHackFn{
func(dcp *DCP) error {
missingURN := "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
if _, ok := dcp.ServiceTypes[missingURN]; ok {
return nil
}
urnParts, err := extractURNParts(missingURN, serviceURNPrefix)
if err != nil {
return err
}
dcp.ServiceTypes[missingURN] = urnParts
return nil
},
},
},
{
Name: "av1",
OfficialName: "MediaServer v1 and MediaRenderer v1",
DocURL: "http://upnp.org/specs/av/av1/",
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
},
}
type DCPHackFn func(*DCP) error
// NAME // NAME
// specgen - generates Go code from the UPnP specification files. // specgen - generates Go code from the UPnP specification files.
// //
@ -35,104 +81,90 @@ var (
// The specification is available for download from: // The specification is available for download from:
// //
// OPTIONS // OPTIONS
// -s, --spec_filename=<upnpresources.zip> // -s, --specs_dir=<spec directory>
// Path to the specification file, available from http://upnp.org/resources/upnpresources.zip // Path to the specification storage directory. This is used to find (and download if not present) the specification ZIP files. Defaults to 'specs'
// -o, --out_dir=<output directory> // -o, --out_dir=<output directory>
// Path to the output directory. This is is where the DCP source files will be placed. Should normally correspond to the directory for github.com/huin/goupnp/dcps // Path to the output directory. This is is where the DCP source files will be placed. Should normally correspond to the directory for github.com/huin/goupnp/dcps. Defaults to '../dcps'
// --nogofmt // --nogofmt
// Disable passing the output through gofmt. Do this if debugging code output problems and needing to see the generated code prior to being passed through gofmt. // Disable passing the output through gofmt. Do this if debugging code output problems and needing to see the generated code prior to being passed through gofmt.
func TaskSpecgen(t *tasking.T) { func TaskSpecgen(t *tasking.T) {
specFilename := t.Flags.String("spec-filename") specsDir := fallbackStrValue("specs", t.Flags.String("specs_dir"), t.Flags.String("s"))
if specFilename == "" { if err := os.MkdirAll(specsDir, os.ModePerm); err != nil {
specFilename = t.Flags.String("s") t.Fatalf("Could not create specs-dir %q: %v\n", specsDir, err)
}
if specFilename == "" {
t.Fatal("--spec_filename is required")
}
outDir := t.Flags.String("out-dir")
if outDir == "" {
outDir = t.Flags.String("o")
}
if outDir == "" {
log.Fatal("--out_dir is required")
} }
outDir := fallbackStrValue("../dcps", t.Flags.String("out_dir"), t.Flags.String("o"))
useGofmt := !t.Flags.Bool("nogofmt") useGofmt := !t.Flags.Bool("nogofmt")
specArchive, err := openZipfile(specFilename) NEXT_DCP:
if err != nil { for _, d := range dcpMetadata {
t.Fatalf("Error opening spec file: %v", err) specFilename := filepath.Join(specsDir, d.Name+".zip")
} err := acquireFile(specFilename, d.XMLSpecURL)
defer specArchive.Close() if err != nil {
t.Logf("Could not acquire spec for %s, skipping: %v\n", d.Name, err)
dcpCol := newDcpsCollection() continue NEXT_DCP
for _, f := range globFiles("standardizeddcps/*/*.zip", specArchive.Reader) {
dirName := strings.TrimPrefix(f.Name, "standardizeddcps/")
slashIndex := strings.Index(dirName, "/")
if slashIndex == -1 {
// Should not happen.
t.Logf("Could not find / in %q", dirName)
return
} }
dirName = dirName[:slashIndex] dcp := newDCP(d)
if err := dcp.processZipFile(specFilename); err != nil {
dcp := dcpCol.dcpForDir(dirName) log.Printf("Error processing spec for %s in file %q: %v", d.Name, specFilename, err)
if dcp == nil { continue NEXT_DCP
t.Logf("No alias defined for directory %q: skipping %s\n", dirName, f.Name)
continue
} else {
t.Logf("Alias found for directory %q: processing %s\n", dirName, f.Name)
} }
for i, hack := range d.Hacks {
dcp.processZipFile(f) if err := hack(dcp); err != nil {
} log.Printf("Error with Hack[%d] for %s: %v", i, d.Name, err)
continue NEXT_DCP
for _, dcp := range dcpCol.dcpByAlias { }
}
dcp.writePackage(outDir, useGofmt)
if err := dcp.writePackage(outDir, useGofmt); err != nil { if err := dcp.writePackage(outDir, useGofmt); err != nil {
log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err) log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err)
continue NEXT_DCP
} }
} }
} }
// DCP contains extra metadata to use when generating DCP source files. func fallbackStrValue(defaultValue string, values ...string) string {
type DCPMetadata struct { for _, v := range values {
Name string // What to name the Go DCP package. if v != "" {
OfficialName string // Official name for the DCP. return v
DocURL string // Optional - URL for futher documentation about the DCP. }
}
var dcpMetadataByDir = map[string]DCPMetadata{
"Internet Gateway_1": {
Name: "internetgateway1",
OfficialName: "Internet Gateway Device v1",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
},
"Internet Gateway_2": {
Name: "internetgateway2",
OfficialName: "Internet Gateway Device v2",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
},
}
type dcpCollection struct {
dcpByAlias map[string]*DCP
}
func newDcpsCollection() *dcpCollection {
c := &dcpCollection{
dcpByAlias: make(map[string]*DCP),
} }
for _, metadata := range dcpMetadataByDir { return defaultValue
c.dcpByAlias[metadata.Name] = newDCP(metadata)
}
return c
} }
func (c *dcpCollection) dcpForDir(dirName string) *DCP { func acquireFile(specFilename string, xmlSpecURL string) error {
metadata, ok := dcpMetadataByDir[dirName] if f, err := os.Open(specFilename); err != nil {
if !ok { if !os.IsNotExist(err) {
return err
}
} else {
f.Close()
return nil return nil
} }
return c.dcpByAlias[metadata.Name]
resp, err := http.Get(xmlSpecURL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not download spec %q from %q: ",
specFilename, xmlSpecURL, resp.Status)
}
tmpFilename := specFilename + ".download"
w, err := os.Create(tmpFilename)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
return os.Rename(tmpFilename, specFilename)
} }
// DCP collects together information about a UPnP Device Control Protocol. // DCP collects together information about a UPnP Device Control Protocol.
@ -151,33 +183,37 @@ func newDCP(metadata DCPMetadata) *DCP {
} }
} }
func (dcp *DCP) processZipFile(file *zip.File) { func (dcp *DCP) processZipFile(filename string) error {
archive, err := openChildZip(file) archive, err := zip.OpenReader(filename)
if err != nil { if err != nil {
log.Println("Error reading child zip file:", err) return fmt.Errorf("error reading zip file %q: %v", filename, err)
return
} }
defer archive.Close()
for _, deviceFile := range globFiles("*/device/*.xml", archive) { for _, deviceFile := range globFiles("*/device/*.xml", archive) {
dcp.processDeviceFile(deviceFile) if err := dcp.processDeviceFile(deviceFile); err != nil {
return err
}
} }
for _, scpdFile := range globFiles("*/service/*.xml", archive) { for _, scpdFile := range globFiles("*/service/*.xml", archive) {
dcp.processSCPDFile(scpdFile) if err := dcp.processSCPDFile(scpdFile); err != nil {
return err
}
} }
return nil
} }
func (dcp *DCP) processDeviceFile(file *zip.File) { func (dcp *DCP) processDeviceFile(file *zip.File) error {
var device goupnp.Device var device goupnp.Device
if err := unmarshalXmlFile(file, &device); err != nil { if err := unmarshalXmlFile(file, &device); err != nil {
log.Printf("Error decoding device XML from file %q: %v", file.Name, err) return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
return
} }
var mainErr error
device.VisitDevices(func(d *goupnp.Device) { device.VisitDevices(func(d *goupnp.Device) {
t := strings.TrimSpace(d.DeviceType) t := strings.TrimSpace(d.DeviceType)
if t != "" { if t != "" {
u, err := extractURNParts(t, deviceURNPrefix) u, err := extractURNParts(t, deviceURNPrefix)
if err != nil { if err != nil {
log.Println(err) mainErr = err
return
} }
dcp.DeviceTypes[t] = u dcp.DeviceTypes[t] = u
} }
@ -185,11 +221,11 @@ func (dcp *DCP) processDeviceFile(file *zip.File) {
device.VisitServices(func(s *goupnp.Service) { device.VisitServices(func(s *goupnp.Service) {
u, err := extractURNParts(s.ServiceType, serviceURNPrefix) u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
if err != nil { if err != nil {
log.Println(err) mainErr = err
return
} }
dcp.ServiceTypes[s.ServiceType] = u dcp.ServiceTypes[s.ServiceType] = u
}) })
return mainErr
} }
func (dcp *DCP) writePackage(outDir string, useGofmt bool) error { func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
@ -217,22 +253,21 @@ func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
return output.Close() return output.Close()
} }
func (dcp *DCP) processSCPDFile(file *zip.File) { func (dcp *DCP) processSCPDFile(file *zip.File) error {
scpd := new(scpd.SCPD) scpd := new(scpd.SCPD)
if err := unmarshalXmlFile(file, scpd); err != nil { if err := unmarshalXmlFile(file, scpd); err != nil {
log.Printf("Error decoding SCPD XML from file %q: %v", file.Name, err) return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
return
} }
scpd.Clean() scpd.Clean()
urnParts, err := urnPartsFromSCPDFilename(file.Name) urnParts, err := urnPartsFromSCPDFilename(file.Name)
if err != nil { if err != nil {
log.Printf("Could not recognize SCPD filename %q: %v", file.Name, err) return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
return
} }
dcp.Services = append(dcp.Services, SCPDWithURN{ dcp.Services = append(dcp.Services, SCPDWithURN{
URNParts: urnParts, URNParts: urnParts,
SCPD: scpd, SCPD: scpd,
}) })
return nil
} }
type SCPDWithURN struct { type SCPDWithURN struct {
@ -240,7 +275,19 @@ type SCPDWithURN struct {
SCPD *scpd.SCPD SCPD *scpd.SCPD
} }
func (s *SCPDWithURN) WrapArgument(arg scpd.Argument) (*argumentWrapper, error) { func (s *SCPDWithURN) WrapArguments(args []*scpd.Argument) (argumentWrapperList, error) {
wrappedArgs := make(argumentWrapperList, len(args))
for i, arg := range args {
wa, err := s.wrapArgument(arg)
if err != nil {
return nil, err
}
wrappedArgs[i] = wa
}
return wrappedArgs, nil
}
func (s *SCPDWithURN) wrapArgument(arg *scpd.Argument) (*argumentWrapper, error) {
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable) relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
if relVar == nil { if relVar == nil {
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name) return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
@ -250,7 +297,7 @@ func (s *SCPDWithURN) WrapArgument(arg scpd.Argument) (*argumentWrapper, error)
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name) return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
} }
return &argumentWrapper{ return &argumentWrapper{
Argument: arg, Argument: *arg,
relVar: relVar, relVar: relVar,
conv: cnv, conv: cnv,
}, nil }, nil
@ -266,6 +313,12 @@ func (arg *argumentWrapper) AsParameter() string {
return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType) return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType)
} }
func (arg *argumentWrapper) HasDoc() bool {
rng := arg.relVar.AllowedValueRange
return ((rng != nil && (rng.Minimum != "" || rng.Maximum != "" || rng.Step != "")) ||
len(arg.relVar.AllowedValues) > 0)
}
func (arg *argumentWrapper) Document() string { func (arg *argumentWrapper) Document() string {
relVar := arg.relVar relVar := arg.relVar
if rng := relVar.AllowedValueRange; rng != nil { if rng := relVar.AllowedValueRange; rng != nil {
@ -295,6 +348,17 @@ func (arg *argumentWrapper) Unmarshal(objVar string) string {
return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name) return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name)
} }
type argumentWrapperList []*argumentWrapper
func (args argumentWrapperList) HasDoc() bool {
for _, arg := range args {
if arg.HasDoc() {
return true
}
}
return false
}
type conv struct { type conv struct {
FuncSuffix string FuncSuffix string
ExtType string ExtType string
@ -325,49 +389,10 @@ var typeConvs = map[string]conv{
"boolean": conv{"Boolean", "bool"}, "boolean": conv{"Boolean", "bool"},
"bin.base64": conv{"BinBase64", "[]byte"}, "bin.base64": conv{"BinBase64", "[]byte"},
"bin.hex": conv{"BinHex", "[]byte"}, "bin.hex": conv{"BinHex", "[]byte"},
"uri": conv{"URI", "*url.URL"},
} }
type closeableZipReader struct { func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
io.Closer
*zip.Reader
}
func openZipfile(filename string) (*closeableZipReader, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
fi, err := file.Stat()
if err != nil {
return nil, err
}
archive, err := zip.NewReader(file, fi.Size())
if err != nil {
return nil, err
}
return &closeableZipReader{
Closer: file,
Reader: archive,
}, nil
}
// openChildZip opens a zip file within another zip file.
func openChildZip(file *zip.File) (*zip.Reader, error) {
zipFile, err := file.Open()
if err != nil {
return nil, err
}
defer zipFile.Close()
zipBytes, err := ioutil.ReadAll(zipFile)
if err != nil {
return nil, err
}
return zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
}
func globFiles(pattern string, archive *zip.Reader) []*zip.File {
var files []*zip.File var files []*zip.File
for _, f := range archive.File { for _, f := range archive.File {
if matched, err := path.Match(pattern, f.Name); err != nil { if matched, err := path.Match(pattern, f.Name); err != nil {
@ -435,14 +460,14 @@ var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metad
// {{if .Metadata.DocURL}} // {{if .Metadata.DocURL}}
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}} // This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
// //
// Typically, use one of the New* functions to discover services on the local // Typically, use one of the New* functions to create clients for services.
// network.
package {{$name}} package {{$name}}
// Generated file - do not edit by hand. See README.md // Generated file - do not edit by hand. See README.md
import ( import (
"net/url"
"time" "time"
"github.com/huin/goupnp" "github.com/huin/goupnp"
@ -484,38 +509,77 @@ func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err er
if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil { if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil {
return return
} }
clients = make([]*{{$srvIdent}}, len(genericClients)) clients = new{{$srvIdent}}ClientsFromGenericClients(genericClients)
return
}
// New{{$srvIdent}}ClientsByURL discovers instances of the service at the given
// URL, and returns clients to any that are found. An error is returned if
// there was an error probing the service.
//
// This is a typical entry calling point into this package when reusing an
// previously discovered service URL.
func New{{$srvIdent}}ClientsByURL(loc *url.URL) ([]*{{$srvIdent}}, error) {
genericClients, err := goupnp.NewServiceClientsByURL(loc, {{$srv.Const}})
if err != nil {
return nil, err
}
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
}
// New{{$srvIdent}}ClientsFromRootDevice discovers instances of the service in
// a given root device, and returns clients to any that are found. An error is
// returned if there was not at least one instance of the service within the
// device. The location parameter is simply assigned to the Location attribute
// of the wrapped ServiceClient(s).
//
// This is a typical entry calling point into this package when reusing an
// previously discovered root device.
func New{{$srvIdent}}ClientsFromRootDevice(rootDevice *goupnp.RootDevice, loc *url.URL) ([]*{{$srvIdent}}, error) {
genericClients, err := goupnp.NewServiceClientsFromRootDevice(rootDevice, loc, {{$srv.Const}})
if err != nil {
return nil, err
}
return new{{$srvIdent}}ClientsFromGenericClients(genericClients), nil
}
func new{{$srvIdent}}ClientsFromGenericClients(genericClients []goupnp.ServiceClient) []*{{$srvIdent}} {
clients := make([]*{{$srvIdent}}, len(genericClients))
for i := range genericClients { for i := range genericClients {
clients[i] = &{{$srvIdent}}{genericClients[i]} clients[i] = &{{$srvIdent}}{genericClients[i]}
} }
return return clients
} }
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}} {{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
{{$inargs := .InputArguments}}{{$outargs := .OutputArguments}} {{$winargs := $srv.WrapArguments .InputArguments}}
// {{if $inargs}}Arguments:{{range $inargs}}{{$argWrap := $srv.WrapArgument .}} {{$woutargs := $srv.WrapArguments .OutputArguments}}
{{if $winargs.HasDoc}}
// //
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}} // Arguments:{{range $winargs}}{{if .HasDoc}}
// //
// {{if $outargs}}Return values:{{range $outargs}}{{$argWrap := $srv.WrapArgument .}} // * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
{{if $woutargs.HasDoc}}
// //
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}} // Return values:{{range $woutargs}}{{if .HasDoc}}
func (client *{{$srvIdent}}) {{.Name}}({{range $inargs}}{{/* //
*/}}{{$argWrap := $srv.WrapArgument .}}{{$argWrap.AsParameter}}, {{end}}{{/* // * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}}
*/}}) ({{range $outargs}}{{/* func (client *{{$srvIdent}}) {{.Name}}({{range $winargs}}{{/*
*/}}{{$argWrap := $srv.WrapArgument .}}{{$argWrap.AsParameter}}, {{end}} err error) { */}}{{.AsParameter}}, {{end}}{{/*
*/}}) ({{range $woutargs}}{{/*
*/}}{{.AsParameter}}, {{end}} err error) {
// Request structure. // Request structure.
request := {{if $inargs}}&{{template "argstruct" $inargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
// BEGIN Marshal arguments into request. // BEGIN Marshal arguments into request.
{{range $inargs}}{{$argWrap := $srv.WrapArgument .}} {{range $winargs}}
if request.{{.Name}}, err = {{$argWrap.Marshal}}; err != nil { if request.{{.Name}}, err = {{.Marshal}}; err != nil {
return return
}{{end}} }{{end}}
// END Marshal arguments into request. // END Marshal arguments into request.
// Response structure. // Response structure.
response := {{if $outargs}}&{{template "argstruct" $outargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
// Perform the SOAP call. // Perform the SOAP call.
if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil { if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil {
@ -523,8 +587,8 @@ func (client *{{$srvIdent}}) {{.Name}}({{range $inargs}}{{/*
} }
// BEGIN Unmarshal arguments from response. // BEGIN Unmarshal arguments from response.
{{range $outargs}}{{$argWrap := $srv.WrapArgument .}} {{range $woutargs}}
if {{.Name}}, err = {{$argWrap.Unmarshal "response"}}; err != nil { if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil {
return return
}{{end}} }{{end}}
// END Unmarshal arguments from response. // END Unmarshal arguments from response.

View File

@ -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,8 +39,16 @@ 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
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 // 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} maybe.Err = ContextError{"unexpected bad location from search", err}
continue continue
} }
locStr := loc.String() maybe.Location = loc
root := new(RootDevice) if root, err := DeviceByURL(loc); err != nil {
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil { maybe.Err = err
maybe.Err = ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
continue
}
var urlBaseStr string
if root.URLBaseStr != "" {
urlBaseStr = root.URLBaseStr
} else { } else {
urlBaseStr = locStr maybe.Root = root
} }
urlBase, err := url.Parse(urlBaseStr)
if err != nil {
maybe.Err = ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
continue
}
root.SetURLBase(urlBase)
maybe.Root = root
} }
return results, nil 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 { func requestXml(url string, defaultSpace string, doc interface{}) error {
timeout := time.Duration(3 * time.Second) timeout := time.Duration(3 * time.Second)
client := http.Client{ client := http.Client{

View File

@ -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,26 +36,50 @@ func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []e
continue continue
} }
device := &maybeRootDevice.Root.Device deviceClients, err := NewServiceClientsFromRootDevice(maybeRootDevice.Root, maybeRootDevice.Location, searchTarget)
srvs := device.FindService(searchTarget) if err != nil {
if len(srvs) == 0 { errors = append(errors, err)
errors = append(errors, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
searchTarget, device.FriendlyName, device.UDN))
continue continue
} }
clients = append(clients, deviceClients...)
for _, srv := range srvs {
clients = append(clients, ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: maybeRootDevice.Root,
Service: srv,
})
}
} }
return 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 // GetServiceClient returns the ServiceClient itself. This is provided so that the
// service client attributes can be accessed via an interface method on a // service client attributes can be accessed via an interface method on a
// wrapping type. // wrapping type.

View File

@ -1,85 +0,0 @@
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)
}
}

View File

@ -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) {
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)
}

View File

@ -1,481 +0,0 @@
package soap
import (
"bytes"
"math"
"testing"
"time"
)
type convTest interface {
Marshal() (string, error)
Unmarshal(string) (interface{}, error)
Equal(result interface{}) bool
}
// duper is an interface that convTest values may optionally also implement to
// generate another convTest for a value in an otherwise identical testCase.
type duper interface {
Dupe(tag string) []convTest
}
type testCase struct {
value convTest
str string
wantMarshalErr bool
wantUnmarshalErr bool
noMarshal bool
noUnMarshal bool
tag string
}
type Ui1Test uint8
func (v Ui1Test) Marshal() (string, error) {
return MarshalUi1(uint8(v))
}
func (v Ui1Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi1(s)
}
func (v Ui1Test) Equal(result interface{}) bool {
return uint8(v) == result.(uint8)
}
func (v Ui1Test) Dupe(tag string) []convTest {
if tag == "dupe" {
return []convTest{
Ui2Test(v),
Ui4Test(v),
}
}
return nil
}
type Ui2Test uint16
func (v Ui2Test) Marshal() (string, error) {
return MarshalUi2(uint16(v))
}
func (v Ui2Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi2(s)
}
func (v Ui2Test) Equal(result interface{}) bool {
return uint16(v) == result.(uint16)
}
type Ui4Test uint32
func (v Ui4Test) Marshal() (string, error) {
return MarshalUi4(uint32(v))
}
func (v Ui4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi4(s)
}
func (v Ui4Test) Equal(result interface{}) bool {
return uint32(v) == result.(uint32)
}
type I1Test int8
func (v I1Test) Marshal() (string, error) {
return MarshalI1(int8(v))
}
func (v I1Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI1(s)
}
func (v I1Test) Equal(result interface{}) bool {
return int8(v) == result.(int8)
}
func (v I1Test) Dupe(tag string) []convTest {
if tag == "dupe" {
return []convTest{
I2Test(v),
I4Test(v),
}
}
return nil
}
type I2Test int16
func (v I2Test) Marshal() (string, error) {
return MarshalI2(int16(v))
}
func (v I2Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI2(s)
}
func (v I2Test) Equal(result interface{}) bool {
return int16(v) == result.(int16)
}
type I4Test int32
func (v I4Test) Marshal() (string, error) {
return MarshalI4(int32(v))
}
func (v I4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI4(s)
}
func (v I4Test) Equal(result interface{}) bool {
return int32(v) == result.(int32)
}
type IntTest int64
func (v IntTest) Marshal() (string, error) {
return MarshalInt(int64(v))
}
func (v IntTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalInt(s)
}
func (v IntTest) Equal(result interface{}) bool {
return int64(v) == result.(int64)
}
type Fixed14_4Test float64
func (v Fixed14_4Test) Marshal() (string, error) {
return MarshalFixed14_4(float64(v))
}
func (v Fixed14_4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalFixed14_4(s)
}
func (v Fixed14_4Test) Equal(result interface{}) bool {
return math.Abs(float64(v)-result.(float64)) < 0.001
}
type CharTest rune
func (v CharTest) Marshal() (string, error) {
return MarshalChar(rune(v))
}
func (v CharTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalChar(s)
}
func (v CharTest) Equal(result interface{}) bool {
return rune(v) == result.(rune)
}
type DateTest struct{ time.Time }
func (v DateTest) Marshal() (string, error) {
return MarshalDate(time.Time(v.Time))
}
func (v DateTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDate(s)
}
func (v DateTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
func (v DateTest) Dupe(tag string) []convTest {
if tag != "no:dateTime" {
return []convTest{DateTimeTest{v.Time}}
}
return nil
}
type TimeOfDayTest struct {
TimeOfDay
}
func (v TimeOfDayTest) Marshal() (string, error) {
return MarshalTimeOfDay(v.TimeOfDay)
}
func (v TimeOfDayTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalTimeOfDay(s)
}
func (v TimeOfDayTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay)
}
func (v TimeOfDayTest) Dupe(tag string) []convTest {
if tag != "no:time.tz" {
return []convTest{TimeOfDayTzTest{v.TimeOfDay}}
}
return nil
}
type TimeOfDayTzTest struct {
TimeOfDay
}
func (v TimeOfDayTzTest) Marshal() (string, error) {
return MarshalTimeOfDayTz(v.TimeOfDay)
}
func (v TimeOfDayTzTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalTimeOfDayTz(s)
}
func (v TimeOfDayTzTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay)
}
type DateTimeTest struct{ time.Time }
func (v DateTimeTest) Marshal() (string, error) {
return MarshalDateTime(time.Time(v.Time))
}
func (v DateTimeTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDateTime(s)
}
func (v DateTimeTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
func (v DateTimeTest) Dupe(tag string) []convTest {
if tag != "no:dateTime.tz" {
return []convTest{DateTimeTzTest{v.Time}}
}
return nil
}
type DateTimeTzTest struct{ time.Time }
func (v DateTimeTzTest) Marshal() (string, error) {
return MarshalDateTimeTz(time.Time(v.Time))
}
func (v DateTimeTzTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDateTimeTz(s)
}
func (v DateTimeTzTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
type BooleanTest bool
func (v BooleanTest) Marshal() (string, error) {
return MarshalBoolean(bool(v))
}
func (v BooleanTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalBoolean(s)
}
func (v BooleanTest) Equal(result interface{}) bool {
return bool(v) == result.(bool)
}
type BinBase64Test []byte
func (v BinBase64Test) Marshal() (string, error) {
return MarshalBinBase64([]byte(v))
}
func (v BinBase64Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalBinBase64(s)
}
func (v BinBase64Test) Equal(result interface{}) bool {
return bytes.Equal([]byte(v), result.([]byte))
}
type BinHexTest []byte
func (v BinHexTest) Marshal() (string, error) {
return MarshalBinHex([]byte(v))
}
func (v BinHexTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalBinHex(s)
}
func (v BinHexTest) Equal(result interface{}) bool {
return bytes.Equal([]byte(v), result.([]byte))
}
func Test(t *testing.T) {
const time010203 time.Duration = (1*3600 + 2*60 + 3) * time.Second
const time0102 time.Duration = (1*3600 + 2*60) * time.Second
const time01 time.Duration = (1 * 3600) * time.Second
const time235959 time.Duration = (23*3600 + 59*60 + 59) * time.Second
// Fake out the local time for the implementation.
localLoc = time.FixedZone("Fake/Local", 6*3600)
defer func() {
localLoc = time.Local
}()
tests := []testCase{
// ui1
{str: "", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: " ", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "abc", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "-1", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "0", value: Ui1Test(0), tag: "dupe"},
{str: "1", value: Ui1Test(1), tag: "dupe"},
{str: "255", value: Ui1Test(255), tag: "dupe"},
{str: "256", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true},
// ui2
{str: "65535", value: Ui2Test(65535)},
{str: "65536", value: Ui2Test(0), wantUnmarshalErr: true, noMarshal: true},
// ui4
{str: "4294967295", value: Ui4Test(4294967295)},
{str: "4294967296", value: Ui4Test(0), wantUnmarshalErr: true, noMarshal: true},
// i1
{str: "", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: " ", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "abc", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "0", value: I1Test(0), tag: "dupe"},
{str: "-1", value: I1Test(-1), tag: "dupe"},
{str: "127", value: I1Test(127), tag: "dupe"},
{str: "-128", value: I1Test(-128), tag: "dupe"},
{str: "128", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-129", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
// i2
{str: "32767", value: I2Test(32767)},
{str: "-32768", value: I2Test(-32768)},
{str: "32768", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-32769", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
// i4
{str: "2147483647", value: I4Test(2147483647)},
{str: "-2147483648", value: I4Test(-2147483648)},
{str: "2147483648", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-2147483649", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
// int
{str: "9223372036854775807", value: IntTest(9223372036854775807)},
{str: "-9223372036854775808", value: IntTest(-9223372036854775808)},
{str: "9223372036854775808", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-9223372036854775809", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
// fixed.14.4
{str: "0.0000", value: Fixed14_4Test(0)},
{str: "1.0000", value: Fixed14_4Test(1)},
{str: "1.2346", value: Fixed14_4Test(1.23456)},
{str: "-1.0000", value: Fixed14_4Test(-1)},
{str: "-1.2346", value: Fixed14_4Test(-1.23456)},
{str: "10000000000000.0000", value: Fixed14_4Test(1e13)},
{str: "100000000000000.0000", value: Fixed14_4Test(1e14), wantMarshalErr: true, wantUnmarshalErr: true},
{str: "-10000000000000.0000", value: Fixed14_4Test(-1e13)},
{str: "-100000000000000.0000", value: Fixed14_4Test(-1e14), wantMarshalErr: true, wantUnmarshalErr: true},
// char
{str: "a", value: CharTest('a')},
{str: "z", value: CharTest('z')},
{str: "\u1234", value: CharTest(0x1234)},
{str: "aa", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
{str: "", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
// date
{str: "2013-10-08", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime"},
{str: "20131008", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true, tag: "no:dateTime"},
{str: "2013-10-08T10:30:50", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
{str: "2013-10-08T10:30:50Z", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
{str: "", value: DateTest{}, wantMarshalErr: true, wantUnmarshalErr: true, noMarshal: true},
{str: "-1", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true},
// time
{str: "00:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}},
{str: "000000", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}, noMarshal: true},
{str: "24:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 24 * time.Hour}}, noMarshal: true}, // ISO8601 special case
{str: "24:01:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "24:00:01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "25:00:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "00:60:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "00:00:60", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "01:02:03", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}},
{str: "010203", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}, noMarshal: true},
{str: "23:59:59", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}},
{str: "235959", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}, noMarshal: true},
{str: "01:02", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
{str: "0102", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
{str: "01", value: TimeOfDayTest{TimeOfDay{FromMidnight: time01}}, noMarshal: true},
{str: "foo 01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "foo\n01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03 foo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03\nfoo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03Z", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
// time.tz
{str: "24:00:01", value: TimeOfDayTzTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "01Z", value: TimeOfDayTzTest{TimeOfDay{time01, true, 0}}, noMarshal: true},
{str: "01:02:03Z", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 0}}},
{str: "01+01", value: TimeOfDayTzTest{TimeOfDay{time01, true, 3600}}, noMarshal: true},
{str: "01:02:03+01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600}}, noMarshal: true},
{str: "01:02:03+01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}},
{str: "01:02:03+0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}, noMarshal: true},
{str: "01:02:03-01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -3600}}, noMarshal: true},
{str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}},
{str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true},
// dateTime
{str: "2013-10-08T00:00:00", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "20131008", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50", value: DateTimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50T", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
// dateTime.tz
{str: "2013-10-08T10:30:50", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}, noMarshal: true},
// boolean
{str: "0", value: BooleanTest(false)},
{str: "1", value: BooleanTest(true)},
{str: "false", value: BooleanTest(false), noMarshal: true},
{str: "true", value: BooleanTest(true), noMarshal: true},
{str: "no", value: BooleanTest(false), noMarshal: true},
{str: "yes", value: BooleanTest(true), noMarshal: true},
{str: "", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "other", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "2", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "-1", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
// bin.base64
{str: "", value: BinBase64Test{}},
{str: "YQ==", value: BinBase64Test("a")},
{str: "TG9uZ2VyIFN0cmluZy4=", value: BinBase64Test("Longer String.")},
{str: "TG9uZ2VyIEFsaWduZWQu", value: BinBase64Test("Longer Aligned.")},
// bin.hex
{str: "", value: BinHexTest{}},
{str: "61", value: BinHexTest("a")},
{str: "4c6f6e67657220537472696e672e", value: BinHexTest("Longer String.")},
{str: "4C6F6E67657220537472696E672E", value: BinHexTest("Longer String."), noMarshal: true},
}
// Generate extra test cases from convTests that implement duper.
var extras []testCase
for i := range tests {
if duper, ok := tests[i].value.(duper); ok {
dupes := duper.Dupe(tests[i].tag)
for _, duped := range dupes {
dupedCase := testCase(tests[i])
dupedCase.value = duped
extras = append(extras, dupedCase)
}
}
}
tests = append(tests, extras...)
for _, test := range tests {
if test.noMarshal {
} else if resultStr, err := test.value.Marshal(); err != nil && !test.wantMarshalErr {
t.Errorf("For %T marshal %v, want %q, got error: %v", test.value, test.value, test.str, err)
} else if err == nil && test.wantMarshalErr {
t.Errorf("For %T marshal %v, want error, got %q", test.value, test.value, resultStr)
} else if err == nil && resultStr != test.str {
t.Errorf("For %T marshal %v, want %q, got %q", test.value, test.value, test.str, resultStr)
}
if test.noUnMarshal {
} else if resultValue, err := test.value.Unmarshal(test.str); err != nil && !test.wantUnmarshalErr {
t.Errorf("For %T unmarshal %q, want %v, got error: %v", test.value, test.str, test.value, err)
} else if err == nil && test.wantUnmarshalErr {
t.Errorf("For %T unmarshal %q, want error, got %v", test.value, test.str, resultValue)
} else if err == nil && !test.value.Equal(resultValue) {
t.Errorf("For %T unmarshal %q, want %v, got %v", test.value, test.str, test.value, resultValue)
}
}
}

View File

@ -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 {
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) {
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,17 +159,73 @@ 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
// maintain the registry of devices and services. // maintain the registry of devices and services.
func (reg *Registry) ServeMessage(r *http.Request) { 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 {
} }
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 {
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
} }

View File

@ -133,6 +133,9 @@ func discoverUPnP() Interface {
return nil return nil
} }
// finds devices matching the given target and calls matcher for all
// advertised services of each device. The first non-nil service found
// is sent into out. If no service matched, nil is sent.
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) { func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
devs, err := goupnp.DiscoverDevices(target) devs, err := goupnp.DiscoverDevices(target)
if err != nil { if err != nil {
@ -148,7 +151,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 {