add support for Open Connectivity Foundation DCPs

This commit is contained in:
mhhcbon 2022-03-21 14:46:32 +01:00 committed by Huin
parent e739c716b4
commit 9af4afce08
14 changed files with 6446 additions and 85 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.zip
*.sublime-workspace
*.download

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
gen:
(cd cmd/goupnpdcpgen/; go install); go generate ./...

View File

@ -2,15 +2,21 @@ package main
import (
"html/template"
"path/filepath"
)
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
var templateFuncs = template.FuncMap{
"base": filepath.Base,
}
var packageTmpl = template.Must(template.New("package").Funcs(templateFuncs).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}}
// {{if .DocURLs}}
// This DCP is documented in detail at: {{range .DocURLs}}
// - {{.}}{{end}}{{end}}
//
// Typically, use one of the New* functions to create clients for services.
package {{$name}}
package {{$name | base}}
// ***********************************************************
// GENERATED FILE - DO NOT EDIT BY HAND. See README.md

View File

@ -16,6 +16,7 @@ import (
// DCP collects together information about a UPnP Device Control Protocol.
type DCP struct {
Metadata DCPMetadata
DocURLs []string
DeviceTypes map[string]*URNParts
ServiceTypes map[string]*URNParts
Services []SCPDWithURN
@ -29,21 +30,32 @@ func newDCP(metadata DCPMetadata) *DCP {
}
}
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) {
func (dcp *DCP) Reset() {
dcp.DocURLs = nil
dcp.DeviceTypes = make(map[string]*URNParts)
dcp.ServiceTypes = make(map[string]*URNParts)
}
func (dcp *DCP) processZipFile(archive []*zip.File, devices, services []string) error {
var f int
for _, devicesGlob := range devices {
for _, deviceFile := range globFiles(devicesGlob, archive) {
if err := dcp.processDeviceFile(deviceFile); err != nil {
return err
}
f++
}
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
}
for _, scpdsGlob := range services {
for _, scpdFile := range globFiles(scpdsGlob, archive) {
if err := dcp.processSCPDFile(scpdFile); err != nil {
return err
}
f++
}
}
if f < 1 {
return fmt.Errorf("no sdcp/device found in %q and %q", devices, services)
}
return nil
}

View File

@ -9,24 +9,22 @@ import (
"os"
"path"
"regexp"
"strings"
)
func acquireFile(specFilename string, xmlSpecURL string) error {
if f, err := os.Open(specFilename); err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
f.Close()
tmpFilename := specFilename + ".download"
defer os.Remove(tmpFilename)
if fileExists(specFilename) {
return nil
}
tmpFilename := specFilename + ".download"
if err := downloadFile(tmpFilename, xmlSpecURL); err != nil {
return err
}
return os.Rename(tmpFilename, specFilename)
return copyFile(specFilename, tmpFilename)
}
func downloadFile(filename, url string) error {
@ -54,10 +52,11 @@ func downloadFile(filename, url string) error {
return w.Close()
}
func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
func globFiles(pattern string, archive []*zip.File) []*zip.File {
var files []*zip.File
for _, f := range archive.File {
if matched, err := path.Match(pattern, f.Name); err != nil {
pattern = strings.ToLower(pattern)
for _, f := range archive {
if matched, err := path.Match(pattern, strings.ToLower(f.Name)); err != nil {
// This shouldn't happen - all patterns are hard-coded, errors in them
// are a programming error.
panic(err)
@ -93,3 +92,30 @@ func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
Version: version,
}, nil
}
func copyFile(dst string, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
return writeFile(dst, f)
}
func writeFile(dst string, r io.ReadCloser) error {
defer r.Close()
f, err := os.Create(dst)
if err != nil {
return err
}
_, err = io.Copy(f, r)
return err
}
func fileExists(p string) bool {
f, err := os.Open(p)
if err != nil {
return !os.IsNotExist(err)
}
f.Close()
return true
}

View File

@ -35,25 +35,19 @@ func run(dcpName, specsDir string, useGofmt bool) error {
return fmt.Errorf("could not create specs-dir %q: %v", specsDir, err)
}
for _, d := range dcpMetadata {
if d.Name != dcpName {
for _, metadata := range dcpMetadata {
if metadata.Name != dcpName {
continue
}
specFilename := filepath.Join(specsDir, d.Name+".zip")
err := acquireFile(specFilename, d.XMLSpecURL)
dcp := newDCP(metadata)
err := metadata.Src.process(".", metadata.Name, dcp)
if err != nil {
return fmt.Errorf("could not acquire spec for %s: %v", d.Name, err)
return fmt.Errorf("error processing spec %s: %v", metadata.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 {
if err := dcp.writeCode(filepath.Base(metadata.Name)+".go", useGofmt); err != nil {
return fmt.Errorf("error writing package %q: %v", dcp.Metadata.Name, err)
}

View File

@ -1,69 +1,127 @@
package main
import (
"strings"
)
// 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
Src dcpProvider
}
var dcpMetadata = []DCPMetadata{
{
Name: "internetgateway1",
OfficialName: "Internet Gateway Device v1",
Src: upnpdotorg{
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},
Hacks: []DCPHackFn{
fixTotalBytes("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1"),
},
},
},
{
Name: "internetgateway2",
OfficialName: "Internet Gateway Device v2",
Src: upnpdotorg{
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 {
fixMissingURN("urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"),
fixTotalBytes("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1"),
},
},
},
{
Name: "av1",
OfficialName: "MediaServer v1 and MediaRenderer v1",
Src: upnpdotorg{
DocURL: "http://upnp.org/specs/av/av1/",
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
},
},
{
Name: "ocf/internetgateway1",
OfficialName: "Internet Gateway Device v1 - Open Connectivity Foundation",
Src: openconnectivitydotorg{
SpecsURL: ocfSpecsURL,
DocPath: "*/DeviceProtection_1/UPnP-gw-*v1*.pdf",
XMLSpecZipPath: "*/DeviceProtection_1/UPnP-gw-IGD-TestFiles-*.zip",
XMLServicePath: []string{"*/service/*1.xml"},
XMLDevicePath: []string{"*/device/*1.xml"},
Hacks: []DCPHackFn{
fixMissingURN("urn:schemas-upnp-org:service:DeviceProtection:1"),
fixMissingURN("urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"),
fixTotalBytes(),
},
},
},
{
Name: "ocf/internetgateway2",
OfficialName: "Internet Gateway Device v2 - Open Connectivity Foundation",
Src: openconnectivitydotorg{
SpecsURL: ocfSpecsURL,
DocPath: "*/Internet Gateway_2/UPnP-gw-*.pdf",
XMLSpecZipPath: "*/Internet Gateway_2/UPnP-gw-IGD-TestFiles-*.zip",
XMLServicePath: []string{"*/service/*1.xml", "*/service/*2.xml"},
XMLDevicePath: []string{"*/device/*1.xml", "*/device/*2.xml"},
Hacks: []DCPHackFn{
fixMissingURN("urn:schemas-upnp-org:service:DeviceProtection:1"),
fixTotalBytes(),
},
},
},
}
func fixTotalBytes(malformedURNs ...string) func(dcp *DCP) error {
malformedVariables := []string{
"TotalBytesSent",
"TotalBytesReceived",
}
return func(dcp *DCP) error {
for _, service := range dcp.Services {
var process bool
for _, malformedURN := range malformedURNs {
if service.URN == malformedURN {
process = true
break
}
}
if process || len(malformedURNs) < 1 {
variables := service.SCPD.StateVariables
for key, variable := range variables {
varName := variable.Name
for _, malformedVariable := range malformedVariables {
if strings.HasSuffix(varName, malformedVariable) {
// Fix size of total bytes which is by default ui4 or maximum 4 GiB.
variable.DataType.Name = "ui8"
variables[key] = variable
}
}
}
}
}
return nil
}
}
func fixMissingURN(missingURNs ...string) func(dcp *DCP) error {
return func(dcp *DCP) error {
for _, missingURN := range missingURNs {
if _, ok := dcp.ServiceTypes[missingURN]; ok {
continue
}
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

View File

@ -0,0 +1,101 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
)
type dcpProvider interface {
process(tmpdir, name string, dcp *DCP) error
}
type upnpdotorg struct {
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
}
func (u upnpdotorg) process(tmpdir, name string, dcp *DCP) error {
dcp.DocURLs = append(dcp.DocURLs, u.DocURL)
specFilename := filepath.Join(tmpdir, name+".zip")
err := acquireFile(specFilename, u.XMLSpecURL)
if err != nil {
return fmt.Errorf("could not acquire spec for %s: %v", name, err)
}
archive, err := zip.OpenReader(specFilename)
if err != nil {
return fmt.Errorf("error reading zip file %q: %v", specFilename, err)
}
defer archive.Close()
if err := dcp.processZipFile(archive.File, []string{"*/device/*.xml"}, []string{"*/service/*.xml"}); err != nil {
return fmt.Errorf("error processing spec file %q: %v", specFilename, err)
}
for i, hack := range u.Hacks {
if err := hack(dcp); err != nil {
return fmt.Errorf("error with Hack[%d] for %s: %v", i, name, err)
}
}
return nil
}
const ocfSpecsURL = "https://openconnectivity.org/upnp-specs/upnpresources.zip"
type openconnectivitydotorg struct {
DocPath string // Optional - Glob to the related documentation about the DCP.
SpecsURL string // The HTTP location of the zip archive containing all XML spec.
XMLSpecZipPath string // Glob to the zip XML spec file within upnpresources.zip.
// Glob to the services XML files within the ZIP matching XMLSpecZipPath.
XMLServicePath []string
// Glob to the devices XML files within the ZIP matching XMLSpecZipPath.
XMLDevicePath []string
// Any special-case functions to run against the DCP before writing it out.
Hacks []DCPHackFn
}
func (o openconnectivitydotorg) process(tmpdir, name string, dcp *DCP) error {
fname := filepath.Base(name)
allSpecsFilename := filepath.Join(tmpdir, "openconnectivitydotorg_"+fname+".zip")
err := acquireFile(allSpecsFilename, o.SpecsURL)
if err != nil {
return fmt.Errorf("could not acquire specs %s: %v", name, err)
}
allSpecsArchive, err := zip.OpenReader(allSpecsFilename)
if err != nil {
return fmt.Errorf("error reading zip file %q: %v", allSpecsFilename, err)
}
specsArchives := globFiles(o.XMLSpecZipPath, allSpecsArchive.File)
if len(specsArchives) < 1 {
return fmt.Errorf("zip archive %q does not contain specifications at %q", allSpecsFilename, o.XMLSpecZipPath)
}
for _, specArchive := range specsArchives {
f, err := specArchive.Open()
if err != nil {
return fmt.Errorf("error reading zip file %q: %v", specArchive.Name, err)
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
archive, err := zip.NewReader(bytes.NewReader(b), specArchive.FileInfo().Size())
if err != nil {
return fmt.Errorf("error reading zip file %q: %v", specArchive.Name, err)
}
if err := dcp.processZipFile(archive.File, o.XMLDevicePath, o.XMLServicePath); err != nil {
return fmt.Errorf("error processing spec file %q: %v", specArchive.Name, err)
}
}
for i, hack := range o.Hacks {
if err := hack(dcp); err != nil {
return fmt.Errorf("error with Hack[%d] for %s: %v", i, name, err)
}
}
for _, d := range globFiles(o.DocPath, allSpecsArchive.File) {
dcp.DocURLs = append(dcp.DocURLs, d.Name)
}
return nil
}

View File

@ -1,6 +1,7 @@
// Client for UPnP Device Control Protocol MediaServer v1 and MediaRenderer v1.
//
// This DCP is documented in detail at: http://upnp.org/specs/av/av1/
// This DCP is documented in detail at:
// - http://upnp.org/specs/av/av1/
//
// Typically, use one of the New* functions to create clients for services.
package av1

View File

@ -1,6 +1,7 @@
// Client for UPnP Device Control Protocol Internet Gateway Device v1.
//
// This DCP is documented in detail at: http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf
// This DCP is documented in detail at:
// - http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf
//
// Typically, use one of the New* functions to create clients for services.
package internetgateway1

View File

@ -1,6 +1,7 @@
// Client for UPnP Device Control Protocol Internet Gateway Device v2.
//
// This DCP is documented in detail at: http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf
// This DCP is documented in detail at:
// - http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf
//
// Typically, use one of the New* functions to create clients for services.
package internetgateway2

View File

@ -0,0 +1,2 @@
//go:generate goupnpdcpgen -dcp_name ocf/internetgateway2
package internetgateway2

File diff suppressed because it is too large Load Diff

2
go.sum
View File

@ -1,6 +1,4 @@
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=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=