From 656e61dfadd241c7cbdd22a023fa81ecb6860ea8 Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sat, 13 Oct 2018 15:04:17 +0100 Subject: [PATCH] Switch to generate code with `go generate`. --- .gitignore | 2 +- README.md | 12 +- cmd/goupnpdcpgen/codetemplate.go | 153 ++++++++ cmd/goupnpdcpgen/dcp.go | 222 +++++++++++ cmd/goupnpdcpgen/fileutil.go | 88 +++++ cmd/goupnpdcpgen/goupnpdcpgen.go | 64 ++++ cmd/goupnpdcpgen/metadata.go | 69 ++++ cmd/goupnpdcpgen/typemap.go | 35 ++ dcps/av1/gen.go | 2 + dcps/internetgateway1/gen.go | 2 + dcps/internetgateway2/gen.go | 2 + go.mod | 3 - go.sum | 6 - gotasks/specgen_task.go | 626 ------------------------------- 14 files changed, 646 insertions(+), 640 deletions(-) create mode 100644 cmd/goupnpdcpgen/codetemplate.go create mode 100644 cmd/goupnpdcpgen/dcp.go create mode 100644 cmd/goupnpdcpgen/fileutil.go create mode 100644 cmd/goupnpdcpgen/goupnpdcpgen.go create mode 100644 cmd/goupnpdcpgen/metadata.go create mode 100644 cmd/goupnpdcpgen/typemap.go create mode 100644 dcps/av1/gen.go create mode 100644 dcps/internetgateway1/gen.go create mode 100644 dcps/internetgateway2/gen.go delete mode 100644 gotasks/specgen_task.go diff --git a/.gitignore b/.gitignore index d22af12..7a6e0eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/gotasks/specs +*.zip *.sublime-workspace \ No newline at end of file diff --git a/README.md b/README.md index 433ba5c..7c63903 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,19 @@ Core components: Regenerating dcps generated source code: ---------------------------------------- -1. Install gotasks: `go get -u github.com/jingweno/gotask` -2. Change to the gotasks directory: `cd gotasks` -3. Run specgen task: `gotask specgen` +1. Build code generator: + + `go get -u github.com/huin/goupnp/cmd/goupnpdcpgen` + +2. Regenerate the code: + + `go generate ./...` 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`, +adding the service to the `dcpMetadata` whitelist in `cmd/goupnpdcpgen/metadata.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 diff --git a/cmd/goupnpdcpgen/codetemplate.go b/cmd/goupnpdcpgen/codetemplate.go new file mode 100644 index 0000000..47416ac --- /dev/null +++ b/cmd/goupnpdcpgen/codetemplate.go @@ -0,0 +1,153 @@ +package main + +import ( + "html/template" +) + +var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}} +// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}. +// {{if .Metadata.DocURL}} +// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}} +// +// Typically, use one of the New* functions to create clients for services. +package {{$name}} + +// *********************************************************** +// GENERATED FILE - DO NOT EDIT BY HAND. See README.md +// *********************************************************** + +import ( + "net/url" + "time" + + "github.com/huin/goupnp" + "github.com/huin/goupnp/soap" +) + +// Hack to avoid Go complaining if time isn't used. +var _ time.Time + +// Device URNs: +const ({{range .DeviceTypes}} + {{.Const}} = "{{.URN}}"{{end}} +) + +// Service URNs: +const ({{range .ServiceTypes}} + {{.Const}} = "{{.URN}}"{{end}} +) + +{{range .Services}} +{{$srv := .}} +{{$srvIdent := printf "%s%s" .Name .Version}} + +// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See +// goupnp.ServiceClient, which contains RootDevice and Service attributes which +// are provided for informational value. +type {{$srvIdent}} struct { + goupnp.ServiceClient +} + +// New{{$srvIdent}}Clients discovers instances of the service on the network, +// and returns clients to any that are found. errors will contain an error for +// any devices that replied but which could not be queried, and err will be set +// if the discovery process failed outright. +// +// This is a typical entry calling point into this package. +func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) { + var genericClients []goupnp.ServiceClient + if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil { + return + } + 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 { + clients[i] = &{{$srvIdent}}{genericClients[i]} + } + return clients +} + +{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}} + +{{$winargs := $srv.WrapArguments .InputArguments}} +{{$woutargs := $srv.WrapArguments .OutputArguments}} +{{if $winargs.HasDoc}} +// +// Arguments:{{range $winargs}}{{if .HasDoc}} +// +// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}} +{{if $woutargs.HasDoc}} +// +// Return values:{{range $woutargs}}{{if .HasDoc}} +// +// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}} +func (client *{{$srvIdent}}) {{.Name}}({{range $winargs -}} +{{.AsParameter}}, {{end -}} +) ({{range $woutargs -}} +{{.AsParameter}}, {{end}} err error) { + // Request structure. + request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} + // BEGIN Marshal arguments into request. +{{range $winargs}} + if request.{{.Name}}, err = {{.Marshal}}; err != nil { + return + }{{end}} + // END Marshal arguments into request. + + // Response structure. + response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} + + // Perform the SOAP call. + if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil { + return + } + + // BEGIN Unmarshal arguments from response. +{{range $woutargs}} + if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil { + return + }{{end}} + // END Unmarshal arguments from response. + return +} +{{end}} +{{end}} + +{{define "argstruct"}}struct {{"{"}} +{{range .}}{{.Name}} string +{{end}}{{"}"}}{{end}} +`)) diff --git a/cmd/goupnpdcpgen/dcp.go b/cmd/goupnpdcpgen/dcp.go new file mode 100644 index 0000000..27c979f --- /dev/null +++ b/cmd/goupnpdcpgen/dcp.go @@ -0,0 +1,222 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "os" + "strings" + + "github.com/huin/goupnp" + "github.com/huin/goupnp/scpd" + "github.com/huin/goutil/codegen" +) + +// DCP collects together information about a UPnP Device Control Protocol. +type DCP struct { + Metadata DCPMetadata + DeviceTypes map[string]*URNParts + ServiceTypes map[string]*URNParts + Services []SCPDWithURN +} + +func newDCP(metadata DCPMetadata) *DCP { + return &DCP{ + Metadata: metadata, + DeviceTypes: make(map[string]*URNParts), + ServiceTypes: make(map[string]*URNParts), + } +} + +func (dcp *DCP) processZipFile(filename string) error { + archive, err := zip.OpenReader(filename) + if err != nil { + return fmt.Errorf("error reading zip file %q: %v", filename, err) + } + defer archive.Close() + for _, deviceFile := range globFiles("*/device/*.xml", archive) { + if err := dcp.processDeviceFile(deviceFile); err != nil { + return err + } + } + for _, scpdFile := range globFiles("*/service/*.xml", archive) { + if err := dcp.processSCPDFile(scpdFile); err != nil { + return err + } + } + return nil +} + +func (dcp *DCP) processDeviceFile(file *zip.File) error { + var device goupnp.Device + if err := unmarshalXmlFile(file, &device); err != nil { + return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err) + } + var mainErr error + device.VisitDevices(func(d *goupnp.Device) { + t := strings.TrimSpace(d.DeviceType) + if t != "" { + u, err := extractURNParts(t, deviceURNPrefix) + if err != nil { + mainErr = err + } + dcp.DeviceTypes[t] = u + } + }) + device.VisitServices(func(s *goupnp.Service) { + u, err := extractURNParts(s.ServiceType, serviceURNPrefix) + if err != nil { + mainErr = err + } + dcp.ServiceTypes[s.ServiceType] = u + }) + return mainErr +} + +func (dcp *DCP) writeCode(outFile string, useGofmt bool) error { + packageFile, err := os.Create(outFile) + if err != nil { + return err + } + var output io.WriteCloser = packageFile + if useGofmt { + if output, err = codegen.NewGofmtWriteCloser(output); err != nil { + packageFile.Close() + return err + } + } + if err = packageTmpl.Execute(output, dcp); err != nil { + output.Close() + return err + } + return output.Close() +} + +func (dcp *DCP) processSCPDFile(file *zip.File) error { + scpd := new(scpd.SCPD) + if err := unmarshalXmlFile(file, scpd); err != nil { + return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err) + } + scpd.Clean() + urnParts, err := urnPartsFromSCPDFilename(file.Name) + if err != nil { + return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err) + } + dcp.Services = append(dcp.Services, SCPDWithURN{ + URNParts: urnParts, + SCPD: scpd, + }) + return nil +} + +type SCPDWithURN struct { + *URNParts + SCPD *scpd.SCPD +} + +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) + if relVar == nil { + return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name) + } + cnv, ok := typeConvs[relVar.DataType.Name] + if !ok { + return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name) + } + return &argumentWrapper{ + Argument: *arg, + relVar: relVar, + conv: cnv, + }, nil +} + +type argumentWrapper struct { + scpd.Argument + relVar *scpd.StateVariable + conv conv +} + +func (arg *argumentWrapper) AsParameter() string { + 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 { + relVar := arg.relVar + if rng := relVar.AllowedValueRange; rng != nil { + var parts []string + if rng.Minimum != "" { + parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum)) + } + if rng.Maximum != "" { + parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum)) + } + if rng.Step != "" { + parts = append(parts, fmt.Sprintf("step=%s", rng.Step)) + } + return "allowed value range: " + strings.Join(parts, ", ") + } + if len(relVar.AllowedValues) != 0 { + return "allowed values: " + strings.Join(relVar.AllowedValues, ", ") + } + return "" +} + +func (arg *argumentWrapper) Marshal() string { + return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name) +} + +func (arg *argumentWrapper) Unmarshal(objVar string) string { + 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 URNParts struct { + URN string + Name string + Version string +} + +func (u *URNParts) Const() string { + return fmt.Sprintf("URN_%s_%s", u.Name, u.Version) +} + +// extractURNParts extracts the name and version from a URN string. +func extractURNParts(urn, expectedPrefix string) (*URNParts, error) { + if !strings.HasPrefix(urn, expectedPrefix) { + return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix) + } + parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("%q does not have a name and version", urn) + } + name, version := parts[0], parts[1] + return &URNParts{urn, name, version}, nil +} diff --git a/cmd/goupnpdcpgen/fileutil.go b/cmd/goupnpdcpgen/fileutil.go new file mode 100644 index 0000000..d5f79cc --- /dev/null +++ b/cmd/goupnpdcpgen/fileutil.go @@ -0,0 +1,88 @@ +package main + +import ( + "archive/zip" + "encoding/xml" + "fmt" + "io" + "net/http" + "os" + "path" + "regexp" +) + +func acquireFile(specFilename string, xmlSpecURL string) error { + if f, err := os.Open(specFilename); err != nil { + if !os.IsNotExist(err) { + return err + } + } else { + f.Close() + return nil + } + + 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) +} + +func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File { + var files []*zip.File + for _, f := range archive.File { + if matched, err := path.Match(pattern, f.Name); err != nil { + // This shouldn't happen - all patterns are hard-coded, errors in them + // are a programming error. + panic(err) + } else if matched { + files = append(files, f) + } + } + return files +} + +func unmarshalXmlFile(file *zip.File, data interface{}) error { + r, err := file.Open() + if err != nil { + return err + } + decoder := xml.NewDecoder(r) + defer r.Close() + return decoder.Decode(data) +} + +var scpdFilenameRe = regexp.MustCompile( + `.*/([a-zA-Z0-9]+)([0-9]+)\.xml`) + +func urnPartsFromSCPDFilename(filename string) (*URNParts, error) { + parts := scpdFilenameRe.FindStringSubmatch(filename) + if len(parts) != 3 { + return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename) + } + name, version := parts[1], parts[2] + return &URNParts{ + URN: serviceURNPrefix + name + ":" + version, + Name: name, + Version: version, + }, nil +} diff --git a/cmd/goupnpdcpgen/goupnpdcpgen.go b/cmd/goupnpdcpgen/goupnpdcpgen.go new file mode 100644 index 0000000..33b6117 --- /dev/null +++ b/cmd/goupnpdcpgen/goupnpdcpgen.go @@ -0,0 +1,64 @@ +// Command to generate DCP package source from the XML specification. +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" +) + +var ( + deviceURNPrefix = "urn:schemas-upnp-org:device:" + serviceURNPrefix = "urn:schemas-upnp-org:service:" +) + +func main() { + var ( + dcpName = flag.String("dcp_name", "", "Name of the DCP to generate.") + specsDir = flag.String("specs_dir", ".", "Path to the specification storage directory. "+ + "This is used to find (and download if not present) the specification ZIP files.") + useGofmt = flag.Bool("gofmt", true, "Pass the generated code through gofmt. "+ + "Disable this if debugging code generation and needing to see the generated code "+ + "prior to being passed through gofmt.") + ) + flag.Parse() + + if err := run(*dcpName, *specsDir, *useGofmt); err != nil { + log.Fatal(err) + } +} + +func run(dcpName, specsDir string, useGofmt bool) error { + if err := os.MkdirAll(specsDir, os.ModePerm); err != nil { + return fmt.Errorf("could not create specs-dir %q: %v\n", specsDir, err) + } + + for _, d := range dcpMetadata { + if d.Name != dcpName { + continue + } + specFilename := filepath.Join(specsDir, d.Name+".zip") + err := acquireFile(specFilename, d.XMLSpecURL) + if err != nil { + return fmt.Errorf("could not acquire spec for %s: %v", d.Name, err) + } + dcp := newDCP(d) + if err := dcp.processZipFile(specFilename); err != nil { + return fmt.Errorf("error processing spec for %s in file %q: %v", d.Name, specFilename, err) + } + for i, hack := range d.Hacks { + if err := hack(dcp); err != nil { + return fmt.Errorf("error with Hack[%d] for %s: %v", i, d.Name, err) + } + } + if err := dcp.writeCode(d.Name+".go", useGofmt); err != nil { + return fmt.Errorf("error writing package %q: %v", dcp.Metadata.Name, err) + } + + return nil + } + + return fmt.Errorf("could not find DCP with name %q", dcpName) +} diff --git a/cmd/goupnpdcpgen/metadata.go b/cmd/goupnpdcpgen/metadata.go new file mode 100644 index 0000000..b11f8e8 --- /dev/null +++ b/cmd/goupnpdcpgen/metadata.go @@ -0,0 +1,69 @@ +package main + +// 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 further 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", + Hacks: []DCPHackFn{totalBytesHack}, + }, + { + 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 + }, totalBytesHack, + }, + }, + { + 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", + }, +} + +func totalBytesHack(dcp *DCP) error { + for _, service := range dcp.Services { + if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" { + variables := service.SCPD.StateVariables + for key, variable := range variables { + varName := variable.Name + if varName == "TotalBytesSent" || varName == "TotalBytesReceived" { + // Fix size of total bytes which is by default ui4 or maximum 4 GiB. + variable.DataType.Name = "ui8" + variables[key] = variable + } + } + + break + } + } + + return nil +} + +type DCPHackFn func(*DCP) error diff --git a/cmd/goupnpdcpgen/typemap.go b/cmd/goupnpdcpgen/typemap.go new file mode 100644 index 0000000..50b02c7 --- /dev/null +++ b/cmd/goupnpdcpgen/typemap.go @@ -0,0 +1,35 @@ +package main + +type conv struct { + FuncSuffix string + ExtType string +} + +// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name +// suffix inside the soap module (e.g "Fixed14_4") and the Go type. +var typeConvs = map[string]conv{ + "ui1": {"Ui1", "uint8"}, + "ui2": {"Ui2", "uint16"}, + "ui4": {"Ui4", "uint32"}, + "ui8": {"Ui8", "uint64"}, + "i1": {"I1", "int8"}, + "i2": {"I2", "int16"}, + "i4": {"I4", "int32"}, + "int": {"Int", "int64"}, + "r4": {"R4", "float32"}, + "r8": {"R8", "float64"}, + "number": {"R8", "float64"}, // Alias for r8. + "fixed.14.4": {"Fixed14_4", "float64"}, + "float": {"R8", "float64"}, + "char": {"Char", "rune"}, + "string": {"String", "string"}, + "date": {"Date", "time.Time"}, + "dateTime": {"DateTime", "time.Time"}, + "dateTime.tz": {"DateTimeTz", "time.Time"}, + "time": {"TimeOfDay", "soap.TimeOfDay"}, + "time.tz": {"TimeOfDayTz", "soap.TimeOfDay"}, + "boolean": {"Boolean", "bool"}, + "bin.base64": {"BinBase64", "[]byte"}, + "bin.hex": {"BinHex", "[]byte"}, + "uri": {"URI", "*url.URL"}, +} diff --git a/dcps/av1/gen.go b/dcps/av1/gen.go new file mode 100644 index 0000000..4c9e66b --- /dev/null +++ b/dcps/av1/gen.go @@ -0,0 +1,2 @@ +//go:generate goupnpdcpgen -dcp_name av1 +package av1 diff --git a/dcps/internetgateway1/gen.go b/dcps/internetgateway1/gen.go new file mode 100644 index 0000000..2b146a3 --- /dev/null +++ b/dcps/internetgateway1/gen.go @@ -0,0 +1,2 @@ +//go:generate goupnpdcpgen -dcp_name internetgateway1 +package internetgateway1 diff --git a/dcps/internetgateway2/gen.go b/dcps/internetgateway2/gen.go new file mode 100644 index 0000000..752058b --- /dev/null +++ b/dcps/internetgateway2/gen.go @@ -0,0 +1,2 @@ +//go:generate goupnpdcpgen -dcp_name internetgateway2 +package internetgateway2 diff --git a/go.mod b/go.mod index 52f389e..e4a078f 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,7 @@ module github.com/huin/goupnp require ( - github.com/codegangsta/cli v1.20.0 // indirect github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 - github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597 - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 golang.org/x/text v0.3.0 // indirect ) diff --git a/go.sum b/go.sum index 1a0bf39..3e75869 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ -github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw= -github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150 h1:vlNjIqmUZ9CMAWsbURYl3a6wZbw7q5RHVvlXTNS/Bs8= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= -github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597 h1:w6OY+MtPjGk9UimnjfSkfC7uon/hHPGl/1dFU4/0PCo= -github.com/jingweno/gotask v0.0.0-20140112180521-104f8017a597/go.mod h1:uZ3nRjkvWkOHt0xvIb3+Fev4KukoMKd7SjpDXQis37s= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/gotasks/specgen_task.go b/gotasks/specgen_task.go deleted file mode 100644 index 0e97cc6..0000000 --- a/gotasks/specgen_task.go +++ /dev/null @@ -1,626 +0,0 @@ -// +build gotask - -package gotasks - -import ( - "archive/zip" - "encoding/xml" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "path/filepath" - "regexp" - "strings" - "text/template" - - "github.com/huin/goupnp" - "github.com/huin/goupnp/scpd" - "github.com/huin/goutil/codegen" - "github.com/jingweno/gotask/tasking" -) - -var ( - deviceURNPrefix = "urn:schemas-upnp-org:device:" - 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 further 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", - Hacks: []DCPHackFn{totalBytesHack}, - }, - { - 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 - }, totalBytesHack, - }, - }, - { - 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", - }, -} - -func totalBytesHack(dcp *DCP) error { - for _, service := range dcp.Services { - if service.URN == "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" { - variables := service.SCPD.StateVariables - for key, variable := range variables { - varName := variable.Name - if varName == "TotalBytesSent" || varName == "TotalBytesReceived" { - // Fix size of total bytes which is by default ui4 or maximum 4 GiB. - variable.DataType.Name = "ui8" - variables[key] = variable - } - } - - break - } - } - - return nil -} - -type DCPHackFn func(*DCP) error - -// NAME -// specgen - generates Go code from the UPnP specification files. -// -// DESCRIPTION -// The specification is available for download from: -// -// OPTIONS -// -s, --specs_dir= -// 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= -// 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 -// 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) { - specsDir := fallbackStrValue("specs", t.Flags.String("specs_dir"), t.Flags.String("s")) - if err := os.MkdirAll(specsDir, os.ModePerm); err != nil { - t.Fatalf("Could not create specs-dir %q: %v\n", specsDir, err) - } - outDir := fallbackStrValue("../dcps", t.Flags.String("out_dir"), t.Flags.String("o")) - useGofmt := !t.Flags.Bool("nogofmt") - -NEXT_DCP: - for _, d := range dcpMetadata { - specFilename := filepath.Join(specsDir, d.Name+".zip") - err := acquireFile(specFilename, d.XMLSpecURL) - if err != nil { - t.Logf("Could not acquire spec for %s, skipping: %v\n", d.Name, err) - continue NEXT_DCP - } - dcp := newDCP(d) - if err := dcp.processZipFile(specFilename); err != nil { - log.Printf("Error processing spec for %s in file %q: %v", d.Name, specFilename, err) - continue NEXT_DCP - } - for i, hack := range d.Hacks { - if err := hack(dcp); err != nil { - log.Printf("Error with Hack[%d] for %s: %v", i, d.Name, err) - continue NEXT_DCP - } - } - dcp.writePackage(outDir, useGofmt) - if err := dcp.writePackage(outDir, useGofmt); err != nil { - log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err) - continue NEXT_DCP - } - } -} - -func fallbackStrValue(defaultValue string, values ...string) string { - for _, v := range values { - if v != "" { - return v - } - } - return defaultValue -} - -func acquireFile(specFilename string, xmlSpecURL string) error { - if f, err := os.Open(specFilename); err != nil { - if !os.IsNotExist(err) { - return err - } - } else { - f.Close() - return nil - } - - 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. -type DCP struct { - Metadata DCPMetadata - DeviceTypes map[string]*URNParts - ServiceTypes map[string]*URNParts - Services []SCPDWithURN -} - -func newDCP(metadata DCPMetadata) *DCP { - return &DCP{ - Metadata: metadata, - DeviceTypes: make(map[string]*URNParts), - ServiceTypes: make(map[string]*URNParts), - } -} - -func (dcp *DCP) processZipFile(filename string) error { - archive, err := zip.OpenReader(filename) - if err != nil { - return fmt.Errorf("error reading zip file %q: %v", filename, err) - } - defer archive.Close() - for _, deviceFile := range globFiles("*/device/*.xml", archive) { - if err := dcp.processDeviceFile(deviceFile); err != nil { - return err - } - } - for _, scpdFile := range globFiles("*/service/*.xml", archive) { - if err := dcp.processSCPDFile(scpdFile); err != nil { - return err - } - } - return nil -} - -func (dcp *DCP) processDeviceFile(file *zip.File) error { - var device goupnp.Device - if err := unmarshalXmlFile(file, &device); err != nil { - return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err) - } - var mainErr error - device.VisitDevices(func(d *goupnp.Device) { - t := strings.TrimSpace(d.DeviceType) - if t != "" { - u, err := extractURNParts(t, deviceURNPrefix) - if err != nil { - mainErr = err - } - dcp.DeviceTypes[t] = u - } - }) - device.VisitServices(func(s *goupnp.Service) { - u, err := extractURNParts(s.ServiceType, serviceURNPrefix) - if err != nil { - mainErr = err - } - dcp.ServiceTypes[s.ServiceType] = u - }) - return mainErr -} - -func (dcp *DCP) writePackage(outDir string, useGofmt bool) error { - packageDirname := filepath.Join(outDir, dcp.Metadata.Name) - err := os.MkdirAll(packageDirname, os.ModePerm) - if err != nil && !os.IsExist(err) { - return err - } - packageFilename := filepath.Join(packageDirname, dcp.Metadata.Name+".go") - packageFile, err := os.Create(packageFilename) - if err != nil { - return err - } - var output io.WriteCloser = packageFile - if useGofmt { - if output, err = codegen.NewGofmtWriteCloser(output); err != nil { - packageFile.Close() - return err - } - } - if err = packageTmpl.Execute(output, dcp); err != nil { - output.Close() - return err - } - return output.Close() -} - -func (dcp *DCP) processSCPDFile(file *zip.File) error { - scpd := new(scpd.SCPD) - if err := unmarshalXmlFile(file, scpd); err != nil { - return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err) - } - scpd.Clean() - urnParts, err := urnPartsFromSCPDFilename(file.Name) - if err != nil { - return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err) - } - dcp.Services = append(dcp.Services, SCPDWithURN{ - URNParts: urnParts, - SCPD: scpd, - }) - return nil -} - -type SCPDWithURN struct { - *URNParts - SCPD *scpd.SCPD -} - -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) - if relVar == nil { - return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name) - } - cnv, ok := typeConvs[relVar.DataType.Name] - if !ok { - return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name) - } - return &argumentWrapper{ - Argument: *arg, - relVar: relVar, - conv: cnv, - }, nil -} - -type argumentWrapper struct { - scpd.Argument - relVar *scpd.StateVariable - conv conv -} - -func (arg *argumentWrapper) AsParameter() string { - 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 { - relVar := arg.relVar - if rng := relVar.AllowedValueRange; rng != nil { - var parts []string - if rng.Minimum != "" { - parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum)) - } - if rng.Maximum != "" { - parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum)) - } - if rng.Step != "" { - parts = append(parts, fmt.Sprintf("step=%s", rng.Step)) - } - return "allowed value range: " + strings.Join(parts, ", ") - } - if len(relVar.AllowedValues) != 0 { - return "allowed values: " + strings.Join(relVar.AllowedValues, ", ") - } - return "" -} - -func (arg *argumentWrapper) Marshal() string { - return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name) -} - -func (arg *argumentWrapper) Unmarshal(objVar string) string { - 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 { - FuncSuffix string - ExtType string -} - -// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name -// suffix inside the soap module (e.g "Fixed14_4") and the Go type. -var typeConvs = map[string]conv{ - "ui1": conv{"Ui1", "uint8"}, - "ui2": conv{"Ui2", "uint16"}, - "ui4": conv{"Ui4", "uint32"}, - "ui8": conv{"Ui8", "uint64"}, - "i1": conv{"I1", "int8"}, - "i2": conv{"I2", "int16"}, - "i4": conv{"I4", "int32"}, - "int": conv{"Int", "int64"}, - "r4": conv{"R4", "float32"}, - "r8": conv{"R8", "float64"}, - "number": conv{"R8", "float64"}, // Alias for r8. - "fixed.14.4": conv{"Fixed14_4", "float64"}, - "float": conv{"R8", "float64"}, - "char": conv{"Char", "rune"}, - "string": conv{"String", "string"}, - "date": conv{"Date", "time.Time"}, - "dateTime": conv{"DateTime", "time.Time"}, - "dateTime.tz": conv{"DateTimeTz", "time.Time"}, - "time": conv{"TimeOfDay", "soap.TimeOfDay"}, - "time.tz": conv{"TimeOfDayTz", "soap.TimeOfDay"}, - "boolean": conv{"Boolean", "bool"}, - "bin.base64": conv{"BinBase64", "[]byte"}, - "bin.hex": conv{"BinHex", "[]byte"}, - "uri": conv{"URI", "*url.URL"}, -} - -func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File { - var files []*zip.File - for _, f := range archive.File { - if matched, err := path.Match(pattern, f.Name); err != nil { - // This shouldn't happen - all patterns are hard-coded, errors in them - // are a programming error. - panic(err) - } else if matched { - files = append(files, f) - } - } - return files -} - -func unmarshalXmlFile(file *zip.File, data interface{}) error { - r, err := file.Open() - if err != nil { - return err - } - decoder := xml.NewDecoder(r) - defer r.Close() - return decoder.Decode(data) -} - -type URNParts struct { - URN string - Name string - Version string -} - -func (u *URNParts) Const() string { - return fmt.Sprintf("URN_%s_%s", u.Name, u.Version) -} - -// extractURNParts extracts the name and version from a URN string. -func extractURNParts(urn, expectedPrefix string) (*URNParts, error) { - if !strings.HasPrefix(urn, expectedPrefix) { - return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix) - } - parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("%q does not have a name and version", urn) - } - name, version := parts[0], parts[1] - return &URNParts{urn, name, version}, nil -} - -var scpdFilenameRe = regexp.MustCompile( - `.*/([a-zA-Z0-9]+)([0-9]+)\.xml`) - -func urnPartsFromSCPDFilename(filename string) (*URNParts, error) { - parts := scpdFilenameRe.FindStringSubmatch(filename) - if len(parts) != 3 { - return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename) - } - name, version := parts[1], parts[2] - return &URNParts{ - URN: serviceURNPrefix + name + ":" + version, - Name: name, - Version: version, - }, nil -} - -var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}} -// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}. -// {{if .Metadata.DocURL}} -// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}} -// -// Typically, use one of the New* functions to create clients for services. -package {{$name}} - -// *********************************************************** -// GENERATED FILE - DO NOT EDIT BY HAND. See README.md -// *********************************************************** - -import ( - "net/url" - "time" - - "github.com/huin/goupnp" - "github.com/huin/goupnp/soap" -) - -// Hack to avoid Go complaining if time isn't used. -var _ time.Time - -// Device URNs: -const ({{range .DeviceTypes}} - {{.Const}} = "{{.URN}}"{{end}} -) - -// Service URNs: -const ({{range .ServiceTypes}} - {{.Const}} = "{{.URN}}"{{end}} -) - -{{range .Services}} -{{$srv := .}} -{{$srvIdent := printf "%s%s" .Name .Version}} - -// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See -// goupnp.ServiceClient, which contains RootDevice and Service attributes which -// are provided for informational value. -type {{$srvIdent}} struct { - goupnp.ServiceClient -} - -// New{{$srvIdent}}Clients discovers instances of the service on the network, -// and returns clients to any that are found. errors will contain an error for -// any devices that replied but which could not be queried, and err will be set -// if the discovery process failed outright. -// -// This is a typical entry calling point into this package. -func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) { - var genericClients []goupnp.ServiceClient - if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil { - return - } - 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 { - clients[i] = &{{$srvIdent}}{genericClients[i]} - } - return clients -} - -{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}} - -{{$winargs := $srv.WrapArguments .InputArguments}} -{{$woutargs := $srv.WrapArguments .OutputArguments}} -{{if $winargs.HasDoc}} -// -// Arguments:{{range $winargs}}{{if .HasDoc}} -// -// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}} -{{if $woutargs.HasDoc}} -// -// Return values:{{range $woutargs}}{{if .HasDoc}} -// -// * {{.Name}}: {{.Document}}{{end}}{{end}}{{end}} -func (client *{{$srvIdent}}) {{.Name}}({{range $winargs -}} -{{.AsParameter}}, {{end -}} -) ({{range $woutargs -}} -{{.AsParameter}}, {{end}} err error) { - // Request structure. - request := {{if $winargs}}&{{template "argstruct" $winargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} - // BEGIN Marshal arguments into request. -{{range $winargs}} - if request.{{.Name}}, err = {{.Marshal}}; err != nil { - return - }{{end}} - // END Marshal arguments into request. - - // Response structure. - response := {{if $woutargs}}&{{template "argstruct" $woutargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}} - - // Perform the SOAP call. - if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil { - return - } - - // BEGIN Unmarshal arguments from response. -{{range $woutargs}} - if {{.Name}}, err = {{.Unmarshal "response"}}; err != nil { - return - }{{end}} - // END Unmarshal arguments from response. - return -} -{{end}} -{{end}} - -{{define "argstruct"}}struct {{"{"}} -{{range .}}{{.Name}} string -{{end}}{{"}"}}{{end}} -`))