diff --git a/goupnp.go b/goupnp.go
index df561f8..e8d6f0b 100644
--- a/goupnp.go
+++ b/goupnp.go
@@ -85,7 +85,10 @@ func DiscoverDevicesCtx(ctx context.Context, searchTarget string) ([]MaybeRootDe
return nil, err
}
defer hcCleanup()
- responses, err := ssdp.SSDPRawSearchCtx(ctx, hc, string(searchTarget), 2, 3)
+
+ searchCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
+ defer cancel()
+ responses, err := ssdp.RawSearch(searchCtx, hc, string(searchTarget), 3)
if err != nil {
return nil, err
}
@@ -148,6 +151,10 @@ func DeviceByURL(loc *url.URL) (*RootDevice, error) {
// but should not be changed after requesting clients.
var CharsetReaderDefault func(charset string, input io.Reader) (io.Reader, error)
+// HTTPClient specifies the http.Client object used when fetching the XML from the UPnP server.
+// HTTPClient defaults the http.DefaultClient. This may be overridden by the importing application.
+var HTTPClientDefault = http.DefaultClient
+
func requestXml(ctx context.Context, url string, defaultSpace string, doc interface{}) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
@@ -157,7 +164,7 @@ func requestXml(ctx context.Context, url string, defaultSpace string, doc interf
return err
}
- resp, err := http.DefaultClient.Do(req)
+ resp, err := HTTPClientDefault.Do(req)
if err != nil {
return err
}
diff --git a/httpu/httpu.go b/httpu/httpu.go
index 808d600..5bb8d67 100644
--- a/httpu/httpu.go
+++ b/httpu/httpu.go
@@ -3,6 +3,7 @@ package httpu
import (
"bufio"
"bytes"
+ "context"
"errors"
"fmt"
"log"
@@ -26,6 +27,27 @@ type ClientInterface interface {
) ([]*http.Response, error)
}
+// ClientInterfaceCtx is the equivalent of ClientInterface, except with methods
+// taking a context.Context parameter.
+type ClientInterfaceCtx interface {
+ // DoWithContext performs a request. If the input request has a
+ // deadline, then that value will be used as the timeout for how long
+ // to wait before returning the responses that were received. If the
+ // request's context is canceled, this method will return immediately.
+ //
+ // If the request's context is never canceled, and does not have a
+ // deadline, then this function WILL NEVER RETURN. You MUST set an
+ // appropriate deadline on the context, or otherwise cancel it when you
+ // want to finish an operation.
+ //
+ // An error is only returned for failing to send the request. Failures
+ // in receipt simply do not add to the resulting responses.
+ DoWithContext(
+ req *http.Request,
+ numSends int,
+ ) ([]*http.Response, error)
+}
+
// HTTPUClient is a client for dealing with HTTPU (HTTP over UDP). Its typical
// function is for HTTPMU, and particularly SSDP.
type HTTPUClient struct {
@@ -34,6 +56,7 @@ type HTTPUClient struct {
}
var _ ClientInterface = &HTTPUClient{}
+var _ ClientInterfaceCtx = &HTTPUClient{}
// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
// purpose.
@@ -75,6 +98,25 @@ func (httpu *HTTPUClient) Do(
req *http.Request,
timeout time.Duration,
numSends int,
+) ([]*http.Response, error) {
+ ctx := req.Context()
+ if timeout > 0 {
+ var cancel func()
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ defer cancel()
+ req = req.WithContext(ctx)
+ }
+
+ return httpu.DoWithContext(req, numSends)
+}
+
+// DoWithContext implements ClientInterfaceCtx.DoWithContext.
+//
+// Make sure to read the documentation on the ClientInterfaceCtx interface
+// regarding cancellation!
+func (httpu *HTTPUClient) DoWithContext(
+ req *http.Request,
+ numSends int,
) ([]*http.Response, error) {
httpu.connLock.Lock()
defer httpu.connLock.Unlock()
@@ -101,10 +143,28 @@ func (httpu *HTTPUClient) Do(
if err != nil {
return nil, err
}
- if err = httpu.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
- return nil, err
+
+ // Handle context deadline/timeout
+ ctx := req.Context()
+ deadline, ok := ctx.Deadline()
+ if ok {
+ if err = httpu.conn.SetDeadline(deadline); err != nil {
+ return nil, err
+ }
}
+ // Handle context cancelation
+ done := make(chan struct{})
+ defer close(done)
+ go func() {
+ select {
+ case <-ctx.Done():
+ // if context is cancelled, stop any connections by setting time in the past.
+ httpu.conn.SetDeadline(time.Now().Add(-time.Second))
+ case <-done:
+ }
+ }()
+
// Send request.
for i := 0; i < numSends; i++ {
if n, err := httpu.conn.WriteTo(requestBuf.Bytes(), destAddr); err != nil {
diff --git a/httpu/multiclient.go b/httpu/multiclient.go
index 463ab7a..5cc65e9 100644
--- a/httpu/multiclient.go
+++ b/httpu/multiclient.go
@@ -49,14 +49,14 @@ func (mc *MultiClient) Do(
}
func (mc *MultiClient) sendRequests(
- results chan<-[]*http.Response,
+ results chan<- []*http.Response,
req *http.Request,
timeout time.Duration,
numSends int,
) error {
tasks := &errgroup.Group{}
for _, d := range mc.delegates {
- d := d // copy for closure
+ d := d // copy for closure
tasks.Go(func() error {
responses, err := d.Do(req, timeout, numSends)
if err != nil {
@@ -68,3 +68,65 @@ func (mc *MultiClient) sendRequests(
}
return tasks.Wait()
}
+
+// MultiClientCtx dispatches requests out to all the delegated clients.
+type MultiClientCtx struct {
+ // The HTTPU clients to delegate to.
+ delegates []ClientInterfaceCtx
+}
+
+var _ ClientInterfaceCtx = &MultiClientCtx{}
+
+// NewMultiClient creates a new MultiClient that delegates to all the given
+// clients.
+func NewMultiClientCtx(delegates []ClientInterfaceCtx) *MultiClientCtx {
+ return &MultiClientCtx{
+ delegates: delegates,
+ }
+}
+
+// DoWithContext implements ClientInterfaceCtx.DoWithContext.
+func (mc *MultiClientCtx) DoWithContext(
+ req *http.Request,
+ numSends int,
+) ([]*http.Response, error) {
+ tasks, ctx := errgroup.WithContext(req.Context())
+ req = req.WithContext(ctx) // so we cancel if the errgroup errors
+ results := make(chan []*http.Response)
+
+ // For each client, send the request to it and collect results.
+ tasks.Go(func() error {
+ defer close(results)
+ return mc.sendRequestsCtx(results, req, numSends)
+ })
+
+ var responses []*http.Response
+ tasks.Go(func() error {
+ for rs := range results {
+ responses = append(responses, rs...)
+ }
+ return nil
+ })
+
+ return responses, tasks.Wait()
+}
+
+func (mc *MultiClientCtx) sendRequestsCtx(
+ results chan<- []*http.Response,
+ req *http.Request,
+ numSends int,
+) error {
+ tasks := &errgroup.Group{}
+ for _, d := range mc.delegates {
+ d := d // copy for closure
+ tasks.Go(func() error {
+ responses, err := d.DoWithContext(req, numSends)
+ if err != nil {
+ return err
+ }
+ results <- responses
+ return nil
+ })
+ }
+ return tasks.Wait()
+}
diff --git a/httpu/serve.go b/httpu/serve.go
index 9f67af8..bac3296 100644
--- a/httpu/serve.go
+++ b/httpu/serve.go
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"regexp"
+ "sync"
)
const (
@@ -73,20 +74,25 @@ func (srv *Server) Serve(l net.PacketConn) error {
if srv.MaxMessageBytes != 0 {
maxMessageBytes = srv.MaxMessageBytes
}
+
+ bufPool := &sync.Pool{
+ New: func() interface{} {
+ return make([]byte, maxMessageBytes)
+ },
+ }
for {
- buf := make([]byte, maxMessageBytes)
+ buf := bufPool.Get().([]byte)
n, peerAddr, err := l.ReadFrom(buf)
if err != nil {
return err
}
- buf = buf[:n]
-
- go func(buf []byte, peerAddr net.Addr) {
+ go func() {
+ defer bufPool.Put(buf)
// At least one router's UPnP implementation has added a trailing space
// after "HTTP/1.1" - trim it.
- buf = trailingWhitespaceRx.ReplaceAllLiteral(buf, crlf)
+ reqBuf := trailingWhitespaceRx.ReplaceAllLiteral(buf[:n], crlf)
- req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(buf)))
+ req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqBuf)))
if err != nil {
log.Printf("httpu: Failed to parse request: %v", err)
return
@@ -94,7 +100,7 @@ func (srv *Server) Serve(l net.PacketConn) error {
req.RemoteAddr = peerAddr.String()
srv.Handler.ServeMessage(req)
// No need to call req.Body.Close - underlying reader is bytes.Buffer.
- }(buf, peerAddr)
+ }()
}
}
diff --git a/network.go b/network.go
index 947d99c..fa262c8 100644
--- a/network.go
+++ b/network.go
@@ -10,14 +10,14 @@ import (
// httpuClient creates a HTTPU client that multiplexes to all multicast-capable
// IPv4 addresses on the host. Returns a function to clean up once the client is
// no longer required.
-func httpuClient() (httpu.ClientInterface, func(), error) {
+func httpuClient() (httpu.ClientInterfaceCtx, func(), error) {
addrs, err := localIPv4MCastAddrs()
if err != nil {
return nil, nil, ctxError(err, "requesting host IPv4 addresses")
}
closers := make([]io.Closer, 0, len(addrs))
- delegates := make([]httpu.ClientInterface, 0, len(addrs))
+ delegates := make([]httpu.ClientInterfaceCtx, 0, len(addrs))
for _, addr := range addrs {
c, err := httpu.NewHTTPUClientAddr(addr)
if err != nil {
@@ -34,7 +34,7 @@ func httpuClient() (httpu.ClientInterface, func(), error) {
}
}
- return httpu.NewMultiClient(delegates), closer, nil
+ return httpu.NewMultiClientCtx(delegates), closer, nil
}
// localIPv2MCastAddrs returns the set of IPv4 addresses on multicast-able
diff --git a/soap/soap.go b/soap/soap.go
index 0d7a758..689f2a4 100644
--- a/soap/soap.go
+++ b/soap/soap.go
@@ -194,9 +194,13 @@ type soapBody struct {
// SOAPFaultError implements error, and contains SOAP fault information.
type SOAPFaultError struct {
- FaultCode string `xml:"faultCode"`
- FaultString string `xml:"faultString"`
+ FaultCode string `xml:"faultcode"`
+ FaultString string `xml:"faultstring"`
Detail struct {
+ UPnPError struct {
+ Errorcode int `xml:"errorCode"`
+ ErrorDescription string `xml:"errorDescription"`
+ } `xml:"UPnPError"`
Raw []byte `xml:",innerxml"`
} `xml:"detail"`
}
diff --git a/soap/soap_test.go b/soap/soap_test.go
index 889a009..d838457 100644
--- a/soap/soap_test.go
+++ b/soap/soap_test.go
@@ -2,10 +2,12 @@ package soap
import (
"bytes"
+ "errors"
"io/ioutil"
"net/http"
"net/url"
"reflect"
+ "strings"
"testing"
)
@@ -87,6 +89,75 @@ func TestActionInputs(t *testing.T) {
}
}
+func TestUPnPError(t *testing.T) {
+ t.Parallel()
+ url, err := url.Parse("http://example.com/soap")
+ if err != nil {
+ t.Fatal(err)
+ }
+ body := `
+
+
+
+ s:Client
+ UPnPError
+
+
+ 725
+ OnlyPermanentLeasesSupported
+
+
+
+
+ `
+ rt := &capturingRoundTripper{
+ resp: &http.Response{
+ StatusCode: 500,
+ ContentLength: int64(len(body)),
+ Body: ioutil.NopCloser(bytes.NewBufferString(body)),
+ },
+ }
+ client := SOAPClient{
+ EndpointURL: *url,
+ HTTPClient: http.Client{
+ Transport: rt,
+ },
+ }
+
+ err = client.PerformAction("mynamespace", "myaction", nil, nil)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if testing.Verbose() {
+ t.Logf("%+v\n", err)
+ }
+ soapErr := &SOAPFaultError{}
+ if ok := errors.As(err, &soapErr); !ok {
+ t.Fatal("expected *SOAPFaultError")
+ }
+ if soapErr.FaultCode != "s:Client" {
+ t.Fatalf("unexpected FaultCode: %s", soapErr.FaultCode)
+ }
+ if soapErr.FaultString != "UPnPError" {
+ t.Fatalf("unexpected FaultString: %s", soapErr.FaultString)
+ }
+ if soapErr.Detail.UPnPError.Errorcode != 725 {
+ t.Fatalf("unexpected UPnPError Errorcode: %d", soapErr.Detail.UPnPError.Errorcode)
+ }
+ if soapErr.Detail.UPnPError.ErrorDescription != "OnlyPermanentLeasesSupported" {
+ t.Fatalf("unexpected UPnPError ErrorDescription: %s",
+ soapErr.Detail.UPnPError.ErrorDescription)
+ }
+
+ if !strings.EqualFold(string(soapErr.Detail.Raw), `
+
+ 725
+ OnlyPermanentLeasesSupported
+
+ `) {
+ t.Fatalf("unexpected Detail.Raw, got:\n%s", string(soapErr.Detail.Raw))
+ }
+}
func TestEscapeXMLText(t *testing.T) {
t.Parallel()
diff --git a/ssdp/ssdp.go b/ssdp/ssdp.go
index 240dfa7..2f318f3 100644
--- a/ssdp/ssdp.go
+++ b/ssdp/ssdp.go
@@ -35,6 +35,15 @@ type HTTPUClient interface {
) ([]*http.Response, error)
}
+// HTTPUClientCtx is an optional interface that will be used to perform
+// HTTP-over-UDP requests if the client implements it.
+type HTTPUClientCtx interface {
+ DoWithContext(
+ req *http.Request,
+ numSends int,
+ ) ([]*http.Response, error)
+}
+
// SSDPRawSearchCtx performs a fairly raw SSDP search request, and returns the
// unique response(s) that it receives. Each response has the requested
// searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
@@ -49,8 +58,64 @@ func SSDPRawSearchCtx(
maxWaitSeconds int,
numSends int,
) ([]*http.Response, error) {
+ req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
+ if err != nil {
+ return nil, err
+ }
+
+ allResponses, err := httpu.Do(req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
+ if err != nil {
+ return nil, err
+ }
+ return processSSDPResponses(searchTarget, allResponses)
+}
+
+// RawSearch performs a fairly raw SSDP search request, and returns the
+// unique response(s) that it receives. Each response has the requested
+// searchTarget, a USN, and a valid location. If the provided context times out
+// or is canceled, the search will be aborted. numSends is the number of
+// requests to send - 3 is a reasonable value for this.
+//
+// The provided context should have a deadline, since the SSDP protocol
+// requires the max wait time be included in search requests. If the context
+// has no deadline, then a default deadline of 3 seconds will be applied.
+func RawSearch(
+ ctx context.Context,
+ httpu HTTPUClientCtx,
+ searchTarget string,
+ numSends int,
+) ([]*http.Response, error) {
+ // We need a timeout value to include in the SSDP request; get it by
+ // checking the deadline on the context.
+ var maxWaitSeconds int
+ if deadline, ok := ctx.Deadline(); ok {
+ maxWaitSeconds = int(deadline.Sub(time.Now()) / time.Second)
+ } else {
+ // Pick a default timeout of 3 seconds if none was provided.
+ maxWaitSeconds = 3
+
+ var cancel func()
+ ctx, cancel = context.WithTimeout(ctx, time.Duration(maxWaitSeconds)*time.Second)
+ defer cancel()
+ }
+
+ req, err := prepareRequest(ctx, searchTarget, maxWaitSeconds)
+ if err != nil {
+ return nil, err
+ }
+
+ allResponses, err := httpu.DoWithContext(req, numSends)
+ if err != nil {
+ return nil, err
+ }
+ return processSSDPResponses(searchTarget, allResponses)
+}
+
+// prepareRequest checks the provided parameters and constructs a SSDP search
+// request to be sent.
+func prepareRequest(ctx context.Context, searchTarget string, maxWaitSeconds int) (*http.Request, error) {
if maxWaitSeconds < 1 {
- return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
+ return nil, errors.New("ssdp: request timeout must be at least 1s")
}
req := (&http.Request{
@@ -67,11 +132,13 @@ func SSDPRawSearchCtx(
"ST": []string{searchTarget},
},
}).WithContext(ctx)
- allResponses, err := httpu.Do(req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
- if err != nil {
- return nil, err
- }
+ return req, nil
+}
+func processSSDPResponses(
+ searchTarget string,
+ allResponses []*http.Response,
+) ([]*http.Response, error) {
isExactSearch := searchTarget != SSDPAll && searchTarget != UPNPRootDevice
seenIDs := make(map[string]bool)
diff --git a/v2alpha/cmd/goupnp2srvgen/main.go b/v2alpha/cmd/goupnp2srvgen/main.go
index ce69770..54e07a6 100644
--- a/v2alpha/cmd/goupnp2srvgen/main.go
+++ b/v2alpha/cmd/goupnp2srvgen/main.go
@@ -23,7 +23,9 @@ import (
"github.com/huin/goupnp/v2alpha/description/typedesc"
"github.com/huin/goupnp/v2alpha/description/xmlsrvdesc"
"github.com/huin/goupnp/v2alpha/soap"
- "github.com/huin/goupnp/v2alpha/soap/types"
+ "golang.org/x/exp/maps"
+
+ soaptypes "github.com/huin/goupnp/v2alpha/soap/types"
)
var (
@@ -31,7 +33,9 @@ var (
outputDir = flag.String("output_dir", "", "Path to directory to write output in.")
srvManifests = flag.String("srv_manifests", "", "Path to srvmanifests.toml")
srvTemplate = flag.String("srv_template", "", "Path to srv.gotemplate.")
- upnpresourcesZip = flag.String("upnpresources_zip", "", "Path to upnpresources.zip.")
+ upnpresourcesZip = flag.String("upnpresources_zip", "",
+ "Path to upnpresources.zip, downloaded from "+
+ "https://openconnectivity.org/upnp-specs/upnpresources.zip.")
)
const soapActionInterface = "SOAPActionInterface"
@@ -50,14 +54,14 @@ func run() error {
}
if *outputDir == "" {
- return errors.New("-output_dir is a required flag.")
+ return errors.New("-output_dir is a required flag")
}
if err := os.MkdirAll(*outputDir, 0); err != nil {
return fmt.Errorf("creating output_dir %q: %w", *outputDir, err)
}
if *srvManifests == "" {
- return errors.New("-srv_manifests is a required flag.")
+ return errors.New("-srv_manifests is a required flag")
}
var manifests DCPSpecManifests
_, err := toml.DecodeFile(*srvManifests, &manifests)
@@ -66,7 +70,7 @@ func run() error {
}
if *srvTemplate == "" {
- return errors.New("-srv_template is a required flag.")
+ return errors.New("-srv_template is a required flag")
}
tmpl, err := template.New(filepath.Base(*srvTemplate)).Funcs(template.FuncMap{
"args": tmplfuncs.Args,
@@ -77,7 +81,7 @@ func run() error {
}
if *upnpresourcesZip == "" {
- return errors.New("-upnpresources_zip is a required flag.")
+ return errors.New("-upnpresources_zip is a required flag")
}
f, err := os.Open(*upnpresourcesZip)
if err != nil {
@@ -91,7 +95,7 @@ func run() error {
// Use default type map for now. Addtional types could be use instead or
// as well as necessary for extended types.
- typeMap := types.TypeMap().Clone()
+ typeMap := soaptypes.TypeMap().Clone()
typeMap[soapActionInterface] = typedesc.TypeDesc{
GoType: reflect.TypeOf((*soap.Action)(nil)).Elem(),
}
@@ -158,7 +162,8 @@ func processService(
return fmt.Errorf("transforming service description: %w", err)
}
- imps, err := accumulateImports(sd, typeMap)
+ imps := newImports()
+ types, err := accumulateTypes(sd, typeMap, imps)
if err != nil {
return err
}
@@ -167,6 +172,7 @@ func processService(
err = tmpl.ExecuteTemplate(buf, "service", tmplArgs{
Manifest: srvManifest,
Imps: imps,
+ Types: types,
SCPD: sd,
})
if err != nil {
@@ -219,14 +225,44 @@ type ServiceManifest struct {
type tmplArgs struct {
Manifest *ServiceManifest
Imps *imports
+ Types *types
SCPD *srvdesc.SCPD
}
type imports struct {
- // Maps from a type name like "ui4" to the `alias.name` for the import.
- TypeByName map[string]typeDesc
// Each required import line, ordered by path.
ImportLines []importItem
+ // aliasByPath maps from import path to its imported alias.
+ aliasByPath map[string]string
+ // nextAlias is the number for the next import alias.
+ nextAlias int
+}
+
+func newImports() *imports {
+ return &imports{
+ aliasByPath: make(map[string]string),
+ nextAlias: 1,
+ }
+}
+
+func (imps *imports) getAliasForPath(path string) string {
+ if alias, ok := imps.aliasByPath[path]; ok {
+ return alias
+ }
+ alias := fmt.Sprintf("pkg%d", imps.nextAlias)
+ imps.nextAlias++
+ imps.ImportLines = append(imps.ImportLines, importItem{
+ Alias: alias,
+ Path: path,
+ })
+ imps.aliasByPath[path] = alias
+ return alias
+}
+
+type types struct {
+ // Maps from a type name like "ui4" to the `alias.name` for the import.
+ TypeByName map[string]typeDesc
+ StringVarDefs []stringVarDef
}
type typeDesc struct {
@@ -239,17 +275,41 @@ type typeDesc struct {
Name string
}
+type stringVarDef struct {
+ Name string
+ AllowedValues []string
+}
+
type importItem struct {
Alias string
Path string
}
-func accumulateImports(srvDesc *srvdesc.SCPD, typeMap typedesc.TypeMap) (*imports, error) {
- typeNames := make(map[string]bool)
- typeNames[soapActionInterface] = true
+// accumulateTypes creates type information, and adds any required imports for
+// them.
+func accumulateTypes(
+ srvDesc *srvdesc.SCPD,
+ typeMap typedesc.TypeMap,
+ imps *imports,
+) (*types, error) {
+ typeNames := make(map[string]struct{})
+ typeNames[soapActionInterface] = struct{}{}
- err := visitTypesSCPD(srvDesc, func(typeName string) {
- typeNames[typeName] = true
+ var stringVarDefs []stringVarDef
+ sortedVarNames := maps.Keys(srvDesc.VariableByName)
+ sort.Strings(sortedVarNames)
+ for _, svName := range sortedVarNames {
+ sv := srvDesc.VariableByName[svName]
+ if sv.DataType == "string" && len(sv.AllowedValues) > 0 {
+ stringVarDefs = append(stringVarDefs, stringVarDef{
+ Name: svName,
+ AllowedValues: srvDesc.VariableByName[svName].AllowedValues,
+ })
+ }
+ }
+
+ err := visitTypesSCPD(srvDesc, func(sv *srvdesc.StateVariable) {
+ typeNames[sv.DataType] = struct{}{}
})
if err != nil {
return nil, err
@@ -257,7 +317,7 @@ func accumulateImports(srvDesc *srvdesc.SCPD, typeMap typedesc.TypeMap) (*import
// Have sorted list of import package paths. Partly for aesthetics of generated code, but also
// to have stable-generated aliases.
- paths := make(map[string]bool)
+ paths := make(map[string]struct{})
for typeName := range typeNames {
t, ok := typeMap[typeName]
if !ok {
@@ -265,29 +325,17 @@ func accumulateImports(srvDesc *srvdesc.SCPD, typeMap typedesc.TypeMap) (*import
}
pkgPath := t.GoType.PkgPath()
if pkgPath == "" {
- // Builtin type, ignore.
+ // Builtin type, no import needed.
continue
}
- paths[pkgPath] = true
- }
- sortedPaths := make([]string, 0, len(paths))
- for path := range paths {
- sortedPaths = append(sortedPaths, path)
+ paths[pkgPath] = struct{}{}
}
+ sortedPaths := maps.Keys(paths)
sort.Strings(sortedPaths)
- // Generate import aliases.
- index := 1
- aliasByPath := make(map[string]string, len(paths))
- importLines := make([]importItem, 0, len(paths))
+ // Generate import aliases in deterministic order.
for _, path := range sortedPaths {
- alias := fmt.Sprintf("pkg%d", index)
- index++
- importLines = append(importLines, importItem{
- Alias: alias,
- Path: path,
- })
- aliasByPath[path] = alias
+ imps.getAliasForPath(path)
}
// Populate typeByName.
@@ -295,28 +343,27 @@ func accumulateImports(srvDesc *srvdesc.SCPD, typeMap typedesc.TypeMap) (*import
for typeName := range typeNames {
goType := typeMap[typeName]
pkgPath := goType.GoType.PkgPath()
- alias := aliasByPath[pkgPath]
td := typeDesc{
Name: goType.GoType.Name(),
}
- if alias == "" {
+ if pkgPath == "" {
// Builtin type.
td.AbsRef = td.Name
td.Ref = td.Name
} else {
td.AbsRef = strconv.Quote(pkgPath) + "." + td.Name
- td.Ref = alias + "." + td.Name
+ td.Ref = imps.getAliasForPath(pkgPath) + "." + td.Name
}
typeByName[typeName] = td
}
- return &imports{
- TypeByName: typeByName,
- ImportLines: importLines,
+ return &types{
+ TypeByName: typeByName,
+ StringVarDefs: stringVarDefs,
}, nil
}
-type typeVisitor func(typeName string)
+type typeVisitor func(sv *srvdesc.StateVariable)
// visitTypesSCPD calls `visitor` with each data type name (e.g. "ui4") referenced
// by action arguments.`
@@ -335,14 +382,14 @@ func visitTypesAction(action *srvdesc.Action, visitor typeVisitor) error {
if err != nil {
return err
}
- visitor(sv.DataType)
+ visitor(sv)
}
for _, arg := range action.OutArgs {
sv, err := arg.RelatedStateVariable()
if err != nil {
return err
}
- visitor(sv.DataType)
+ visitor(sv)
}
return nil
}
diff --git a/v2alpha/description/srvdesc/srvdesc.go b/v2alpha/description/srvdesc/srvdesc.go
index 4e36c41..103be30 100644
--- a/v2alpha/description/srvdesc/srvdesc.go
+++ b/v2alpha/description/srvdesc/srvdesc.go
@@ -10,9 +10,9 @@ import (
)
var (
- BadDescriptionError = errors.New("bad XML description")
- MissingDefinitionError = errors.New("missing definition")
- UnsupportedDescriptionError = errors.New("unsupported XML description")
+ ErrBadDescription = errors.New("bad XML description")
+ ErrMissingDefinition = errors.New("missing definition")
+ ErrUnsupportedDescription = errors.New("unsupported XML description")
)
// SCPD is the top level service description.
@@ -37,7 +37,7 @@ func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) {
}
if _, exists := stateVariables[sv.Name]; exists {
return nil, fmt.Errorf("%w: multiple state variables with name %q",
- BadDescriptionError, sv.Name)
+ ErrBadDescription, sv.Name)
}
stateVariables[sv.Name] = sv
}
@@ -49,7 +49,7 @@ func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) {
}
if _, exists := actions[action.Name]; exists {
return nil, fmt.Errorf("%w: multiple actions with name %q",
- BadDescriptionError, action.Name)
+ ErrBadDescription, action.Name)
}
actions[action.Name] = action
}
@@ -79,7 +79,7 @@ type Action struct {
// actionFromXML creates an Action from the given XML description.
func actionFromXML(xmlAction *xmlsrvdesc.Action, scpd *SCPD) (*Action, error) {
if xmlAction.Name == "" {
- return nil, fmt.Errorf("%w: empty action name", BadDescriptionError)
+ return nil, fmt.Errorf("%w: empty action name", ErrBadDescription)
}
action := &Action{
SCPD: scpd,
@@ -99,7 +99,7 @@ func actionFromXML(xmlAction *xmlsrvdesc.Action, scpd *SCPD) (*Action, error) {
outArgs = append(outArgs, arg)
default:
return nil, fmt.Errorf("%w: argument %q has invalid direction %q",
- BadDescriptionError, xmlArg.Name, xmlArg.Direction)
+ ErrBadDescription, xmlArg.Name, xmlArg.Direction)
}
}
action.InArgs = inArgs
@@ -117,10 +117,10 @@ type Argument struct {
// argumentFromXML creates an Argument from the XML description.
func argumentFromXML(xmlArg *xmlsrvdesc.Argument, action *Action) (*Argument, error) {
if xmlArg.Name == "" {
- return nil, fmt.Errorf("%w: empty argument name", BadDescriptionError)
+ return nil, fmt.Errorf("%w: empty argument name", ErrBadDescription)
}
if xmlArg.RelatedStateVariable == "" {
- return nil, fmt.Errorf("%w: empty related state variable", BadDescriptionError)
+ return nil, fmt.Errorf("%w: empty related state variable", ErrBadDescription)
}
return &Argument{
Action: action,
@@ -133,25 +133,32 @@ func (arg *Argument) RelatedStateVariable() (*StateVariable, error) {
if v, ok := arg.Action.SCPD.VariableByName[arg.RelatedStateVariableName]; ok {
return v, nil
}
- return nil, fmt.Errorf("%w: state variable %q", MissingDefinitionError, arg.RelatedStateVariableName)
+ return nil, fmt.Errorf("%w: state variable %q", ErrMissingDefinition, arg.RelatedStateVariableName)
}
// StateVariable description data.
type StateVariable struct {
Name string
DataType string
+
+ AllowedValues []string
}
func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) {
if xmlSV.Name == "" {
- return nil, fmt.Errorf("%w: empty state variable name", BadDescriptionError)
+ return nil, fmt.Errorf("%w: empty state variable name", ErrBadDescription)
}
if xmlSV.DataType.Type != "" {
return nil, fmt.Errorf("%w: unsupported data type %q",
- UnsupportedDescriptionError, xmlSV.DataType.Type)
+ ErrUnsupportedDescription, xmlSV.DataType.Type)
+ }
+ if xmlSV.DataType.Name != "string" && len(xmlSV.AllowedValues) > 0 {
+ return nil, fmt.Errorf("%w: allowedValueList is currently unsupported for type %q",
+ ErrUnsupportedDescription, xmlSV.DataType.Name)
}
return &StateVariable{
- Name: xmlSV.Name,
- DataType: xmlSV.DataType.Name,
+ Name: xmlSV.Name,
+ DataType: xmlSV.DataType.Name,
+ AllowedValues: xmlSV.AllowedValues,
}, nil
}
diff --git a/v2alpha/go.mod b/v2alpha/go.mod
index f41ed54..02bc016 100644
--- a/v2alpha/go.mod
+++ b/v2alpha/go.mod
@@ -2,6 +2,8 @@ module github.com/huin/goupnp/v2alpha
go 1.18
-require github.com/google/go-cmp v0.5.7
+require github.com/google/go-cmp v0.5.8
require github.com/BurntSushi/toml v1.1.0
+
+require golang.org/x/exp v0.0.0-20230307190834-24139beb5833 // indirect
diff --git a/v2alpha/go.sum b/v2alpha/go.sum
index b08ca62..e0d90cd 100644
--- a/v2alpha/go.sum
+++ b/v2alpha/go.sum
@@ -2,5 +2,9 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
+golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/v2alpha/soap/client/client.go b/v2alpha/soap/client/client.go
index c608188..70b459f 100644
--- a/v2alpha/soap/client/client.go
+++ b/v2alpha/soap/client/client.go
@@ -8,15 +8,52 @@ import (
"fmt"
"io"
"net/http"
+ "strings"
"github.com/huin/goupnp/v2alpha/soap"
"github.com/huin/goupnp/v2alpha/soap/envelope"
)
-var _ HttpClient = &http.Client{}
+var (
+ // ErrSOAP can be used with errors.Is.
+ ErrSOAP = errors.New("SOAP error")
+)
-// HttpClient defines the interface required of an HTTP client. It is a subset of *http.Client.
-type HttpClient interface {
+// SOAPError describes an error from this package, potentially including a
+// lower-level cause.
+type SOAPError struct {
+ // description describes the error from the SOAP perspective.
+ description string
+ // cause may be nil.
+ cause error
+}
+
+func (se *SOAPError) Error() string {
+ b := &strings.Builder{}
+ b.WriteString("SOAP error")
+ if se.description != "" {
+ b.WriteString(": ")
+ b.WriteString(se.description)
+ }
+ if se.cause != nil {
+ b.WriteString(": ")
+ b.WriteString(se.cause.Error())
+ }
+ return b.String()
+}
+
+func (se *SOAPError) Is(target error) bool {
+ return target == ErrSOAP
+}
+
+func (se *SOAPError) Unwrap() error {
+ return se.cause
+}
+
+var _ HTTPClient = &http.Client{}
+
+// HTTPClient defines the interface required of an HTTP client. It is a subset of *http.Client.
+type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
@@ -25,22 +62,21 @@ type Option func(*options)
// WithHTTPClient specifies an *http.Client to use instead of
// http.DefaultClient.
-func WithHTTPClient(httpClient HttpClient) Option {
+func WithHTTPClient(httpClient HTTPClient) Option {
return func(o *options) {
o.httpClient = httpClient
}
}
type options struct {
- httpClient HttpClient
+ httpClient HTTPClient
}
// Client is a SOAP client, attached to a specific SOAP endpoint.
// the zero value is not usable, use NewClient() to create an instance.
type Client struct {
- httpClient HttpClient
- endpointURL string
- maxErrorResponseBytes int
+ httpClient HTTPClient
+ endpointURL string
}
// New creates a new SOAP client, which will POST its requests to the
@@ -79,8 +115,10 @@ func (c *Client) Do(
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("SOAP request got HTTP %s (%d)",
- resp.Status, resp.StatusCode)
+ return &SOAPError{
+ description: fmt.Sprintf("SOAP request got HTTP %s (%d)",
+ resp.Status, resp.StatusCode),
+ }
}
return ParseResponseAction(resp, actionOut)
@@ -110,7 +148,10 @@ func SetRequestAction(
buf := &bytes.Buffer{}
err := envelope.Write(buf, actionIn)
if err != nil {
- return fmt.Errorf("encoding envelope: %w", err)
+ return &SOAPError{
+ description: "encoding envelope",
+ cause: err,
+ }
}
req.Body = io.NopCloser(buf)
@@ -131,31 +172,39 @@ func ParseResponseAction(
actionOut *envelope.Action,
) error {
if resp.Body == nil {
- return errors.New("missing response body")
+ return &SOAPError{description: "missing HTTP response body"}
}
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, resp.Body); err != nil {
- return fmt.Errorf("reading response body: %w", err)
+ return &SOAPError{
+ description: "reading HTTP response body",
+ cause: err,
+ }
}
if err := envelope.Read(buf, actionOut); err != nil {
- if _, ok := err.(*envelope.Fault); ok {
+ if errors.Is(err, envelope.ErrFault) {
// Parsed cleanly, got SOAP fault.
- return err
+ return &SOAPError{
+ description: "SOAP fault",
+ cause: err,
+ }
}
// Parsing problem, provide some information for context.
dispLen := buf.Len()
truncMessage := ""
if dispLen > 1024 {
dispLen = 1024
- truncMessage = fmt.Sprintf("first %d bytes: ", dispLen)
+ truncMessage = fmt.Sprintf("first %d bytes (total %d bytes): ", dispLen, buf.Len())
+ }
+ return &SOAPError{
+ description: fmt.Sprintf(
+ "parsing SOAP response from HTTP body (%s%q)",
+ truncMessage, buf.Bytes()[:dispLen],
+ ),
+ cause: err,
}
- return fmt.Errorf(
- "parsing response body (%s%q): %w",
- truncMessage, buf.Bytes()[:dispLen],
- err,
- )
}
return nil
diff --git a/v2alpha/soap/types/types.go b/v2alpha/soap/types/types.go
index c34c649..c801601 100644
--- a/v2alpha/soap/types/types.go
+++ b/v2alpha/soap/types/types.go
@@ -321,9 +321,9 @@ var _ SOAPValue = &Fixed14_4{}
// Fixed14_4FromParts creates a Fixed14_4 from components.
// Bounds:
-// * Both intPart and fracPart must have the same sign.
-// * -1e14 < intPart < 1e14
-// * -1e4 < fracPart < 1e4
+// - Both intPart and fracPart must have the same sign.
+// - -1e14 < intPart < 1e14
+// - -1e4 < fracPart < 1e4
func Fixed14_4FromParts(intPart int64, fracPart int16) (Fixed14_4, error) {
var v Fixed14_4
err := v.SetParts(intPart, fracPart)
@@ -332,9 +332,9 @@ func Fixed14_4FromParts(intPart int64, fracPart int16) (Fixed14_4, error) {
// SetFromParts sets the value based on the integer component and the fractional component.
// Bounds:
-// * Both intPart and fracPart must have the same sign.
-// * -1e14 < intPart < 1e14
-// * -1e4 < fracPart < 1e4
+// - Both intPart and fracPart must have the same sign.
+// - -1e14 < intPart < 1e14
+// - -1e4 < fracPart < 1e4
func (v *Fixed14_4) SetParts(intPart int64, fracPart int16) error {
if (intPart < 0) != (fracPart < 0) {
return fmt.Errorf("want intPart and fracPart with same sign, got %d and %d",
diff --git a/v2alpha/srv/inetgw2/lanhostcfgmgmt1/lanhostcfgmgmt1.go b/v2alpha/srv/inetgw2/lanhostcfgmgmt1/lanhostcfgmgmt1.go
index 94b61c4..c1215e0 100755
--- a/v2alpha/srv/inetgw2/lanhostcfgmgmt1/lanhostcfgmgmt1.go
+++ b/v2alpha/srv/inetgw2/lanhostcfgmgmt1/lanhostcfgmgmt1.go
@@ -34,6 +34,7 @@ func (a *DeleteDNSServer) RefResponse() any { return &a.Response }
// DeleteDNSServerRequest contains the "in" args for the "DeleteDNSServer" action.
type DeleteDNSServerRequest struct {
+ // NewDNSServers relates to state variable DNSServers.
NewDNSServers string
}
@@ -64,6 +65,7 @@ func (a *DeleteIPRouter) RefResponse() any { return &a.Response }
// DeleteIPRouterRequest contains the "in" args for the "DeleteIPRouter" action.
type DeleteIPRouterRequest struct {
+ // NewIPRouters relates to state variable IPRouters.
NewIPRouters string
}
@@ -94,6 +96,7 @@ func (a *DeleteReservedAddress) RefResponse() any { return &a.Response }
// DeleteReservedAddressRequest contains the "in" args for the "DeleteReservedAddress" action.
type DeleteReservedAddressRequest struct {
+ // NewReservedAddresses relates to state variable ReservedAddresses.
NewReservedAddresses string
}
@@ -127,7 +130,9 @@ type GetAddressRangeRequest struct{}
// GetAddressRangeResponse contains the "out" args for the "GetAddressRange" action.
type GetAddressRangeResponse struct {
+ // NewMinAddress relates to state variable MinAddress.
NewMinAddress string
+ // NewMaxAddress relates to state variable MaxAddress.
NewMaxAddress string
}
@@ -158,6 +163,7 @@ type GetDHCPRelayRequest struct{}
// GetDHCPRelayResponse contains the "out" args for the "GetDHCPRelay" action.
type GetDHCPRelayResponse struct {
+ // NewDHCPRelay relates to state variable DHCPRelay.
NewDHCPRelay pkg2.Boolean
}
@@ -188,6 +194,7 @@ type GetDHCPServerConfigurableRequest struct{}
// GetDHCPServerConfigurableResponse contains the "out" args for the "GetDHCPServerConfigurable" action.
type GetDHCPServerConfigurableResponse struct {
+ // NewDHCPServerConfigurable relates to state variable DHCPServerConfigurable.
NewDHCPServerConfigurable pkg2.Boolean
}
@@ -218,6 +225,7 @@ type GetDNSServersRequest struct{}
// GetDNSServersResponse contains the "out" args for the "GetDNSServers" action.
type GetDNSServersResponse struct {
+ // NewDNSServers relates to state variable DNSServers.
NewDNSServers string
}
@@ -248,6 +256,7 @@ type GetDomainNameRequest struct{}
// GetDomainNameResponse contains the "out" args for the "GetDomainName" action.
type GetDomainNameResponse struct {
+ // NewDomainName relates to state variable DomainName.
NewDomainName string
}
@@ -278,6 +287,7 @@ type GetIPRoutersListRequest struct{}
// GetIPRoutersListResponse contains the "out" args for the "GetIPRoutersList" action.
type GetIPRoutersListResponse struct {
+ // NewIPRouters relates to state variable IPRouters.
NewIPRouters string
}
@@ -308,6 +318,7 @@ type GetReservedAddressesRequest struct{}
// GetReservedAddressesResponse contains the "out" args for the "GetReservedAddresses" action.
type GetReservedAddressesResponse struct {
+ // NewReservedAddresses relates to state variable ReservedAddresses.
NewReservedAddresses string
}
@@ -338,6 +349,7 @@ type GetSubnetMaskRequest struct{}
// GetSubnetMaskResponse contains the "out" args for the "GetSubnetMask" action.
type GetSubnetMaskResponse struct {
+ // NewSubnetMask relates to state variable SubnetMask.
NewSubnetMask string
}
@@ -365,7 +377,9 @@ func (a *SetAddressRange) RefResponse() any { return &a.Response }
// SetAddressRangeRequest contains the "in" args for the "SetAddressRange" action.
type SetAddressRangeRequest struct {
+ // NewMinAddress relates to state variable MinAddress.
NewMinAddress string
+ // NewMaxAddress relates to state variable MaxAddress.
NewMaxAddress string
}
@@ -396,6 +410,7 @@ func (a *SetDHCPRelay) RefResponse() any { return &a.Response }
// SetDHCPRelayRequest contains the "in" args for the "SetDHCPRelay" action.
type SetDHCPRelayRequest struct {
+ // NewDHCPRelay relates to state variable DHCPRelay.
NewDHCPRelay pkg2.Boolean
}
@@ -426,6 +441,7 @@ func (a *SetDHCPServerConfigurable) RefResponse() any { return &a.Response }
// SetDHCPServerConfigurableRequest contains the "in" args for the "SetDHCPServerConfigurable" action.
type SetDHCPServerConfigurableRequest struct {
+ // NewDHCPServerConfigurable relates to state variable DHCPServerConfigurable.
NewDHCPServerConfigurable pkg2.Boolean
}
@@ -456,6 +472,7 @@ func (a *SetDNSServer) RefResponse() any { return &a.Response }
// SetDNSServerRequest contains the "in" args for the "SetDNSServer" action.
type SetDNSServerRequest struct {
+ // NewDNSServers relates to state variable DNSServers.
NewDNSServers string
}
@@ -486,6 +503,7 @@ func (a *SetDomainName) RefResponse() any { return &a.Response }
// SetDomainNameRequest contains the "in" args for the "SetDomainName" action.
type SetDomainNameRequest struct {
+ // NewDomainName relates to state variable DomainName.
NewDomainName string
}
@@ -516,6 +534,7 @@ func (a *SetIPRouter) RefResponse() any { return &a.Response }
// SetIPRouterRequest contains the "in" args for the "SetIPRouter" action.
type SetIPRouterRequest struct {
+ // NewIPRouters relates to state variable IPRouters.
NewIPRouters string
}
@@ -546,6 +565,7 @@ func (a *SetReservedAddress) RefResponse() any { return &a.Response }
// SetReservedAddressRequest contains the "in" args for the "SetReservedAddress" action.
type SetReservedAddressRequest struct {
+ // NewReservedAddresses relates to state variable ReservedAddresses.
NewReservedAddresses string
}
@@ -576,6 +596,7 @@ func (a *SetSubnetMask) RefResponse() any { return &a.Response }
// SetSubnetMaskRequest contains the "in" args for the "SetSubnetMask" action.
type SetSubnetMaskRequest struct {
+ // NewSubnetMask relates to state variable SubnetMask.
NewSubnetMask string
}
diff --git a/v2alpha/srv/inetgw2/wanpppconn1/wanpppconn1.go b/v2alpha/srv/inetgw2/wanpppconn1/wanpppconn1.go
index b134f3b..c2df357 100644
--- a/v2alpha/srv/inetgw2/wanpppconn1/wanpppconn1.go
+++ b/v2alpha/srv/inetgw2/wanpppconn1/wanpppconn1.go
@@ -8,6 +8,35 @@ import (
pkg2 "github.com/huin/goupnp/v2alpha/soap/types"
)
+// Allowed values for state variable ConnectionStatus.
+const (
+ ConnectionStatus_Unconfigured = "Unconfigured"
+ ConnectionStatus_Connected = "Connected"
+ ConnectionStatus_Disconnected = "Disconnected"
+)
+
+// Allowed values for state variable LastConnectionError.
+const (
+ LastConnectionError_ERROR_NONE = "ERROR_NONE"
+)
+
+// Allowed values for state variable PortMappingProtocol.
+const (
+ PortMappingProtocol_TCP = "TCP"
+ PortMappingProtocol_UDP = "UDP"
+)
+
+// Allowed values for state variable PossibleConnectionTypes.
+const (
+ PossibleConnectionTypes_Unconfigured = "Unconfigured"
+ PossibleConnectionTypes_IP_Routed = "IP_Routed"
+ PossibleConnectionTypes_DHCP_Spoofed = "DHCP_Spoofed"
+ PossibleConnectionTypes_PPPoE_Bridged = "PPPoE_Bridged"
+ PossibleConnectionTypes_PPTP_Relay = "PPTP_Relay"
+ PossibleConnectionTypes_L2TP_Relay = "L2TP_Relay"
+ PossibleConnectionTypes_PPPoE_Relay = "PPPoE_Relay"
+)
+
const ServiceType = "urn:schemas-upnp-org:service:WANPPPConnection:1"
// AddPortMapping provides request and response for the action.
@@ -34,14 +63,22 @@ func (a *AddPortMapping) RefResponse() any { return &a.Response }
// AddPortMappingRequest contains the "in" args for the "AddPortMapping" action.
type AddPortMappingRequest struct {
- NewRemoteHost string
- NewExternalPort pkg2.UI2
- NewProtocol string
- NewInternalPort pkg2.UI2
- NewInternalClient string
- NewEnabled pkg2.Boolean
+ // NewRemoteHost relates to state variable RemoteHost.
+ NewRemoteHost string
+ // NewExternalPort relates to state variable ExternalPort.
+ NewExternalPort pkg2.UI2
+ // NewProtocol relates to state variable PortMappingProtocol (2 standard allowed values).
+ NewProtocol string
+ // NewInternalPort relates to state variable InternalPort.
+ NewInternalPort pkg2.UI2
+ // NewInternalClient relates to state variable InternalClient.
+ NewInternalClient string
+ // NewEnabled relates to state variable PortMappingEnabled.
+ NewEnabled pkg2.Boolean
+ // NewPortMappingDescription relates to state variable PortMappingDescription.
NewPortMappingDescription string
- NewLeaseDuration pkg2.UI4
+ // NewLeaseDuration relates to state variable PortMappingLeaseDuration.
+ NewLeaseDuration pkg2.UI4
}
// AddPortMappingResponse contains the "out" args for the "AddPortMapping" action.
@@ -71,7 +108,9 @@ func (a *ConfigureConnection) RefResponse() any { return &a.Response }
// ConfigureConnectionRequest contains the "in" args for the "ConfigureConnection" action.
type ConfigureConnectionRequest struct {
+ // NewUserName relates to state variable UserName.
NewUserName string
+ // NewPassword relates to state variable Password.
NewPassword string
}
@@ -102,9 +141,12 @@ func (a *DeletePortMapping) RefResponse() any { return &a.Response }
// DeletePortMappingRequest contains the "in" args for the "DeletePortMapping" action.
type DeletePortMappingRequest struct {
- NewRemoteHost string
+ // NewRemoteHost relates to state variable RemoteHost.
+ NewRemoteHost string
+ // NewExternalPort relates to state variable ExternalPort.
NewExternalPort pkg2.UI2
- NewProtocol string
+ // NewProtocol relates to state variable PortMappingProtocol (2 standard allowed values).
+ NewProtocol string
}
// DeletePortMappingResponse contains the "out" args for the "DeletePortMapping" action.
@@ -165,6 +207,7 @@ type GetAutoDisconnectTimeRequest struct{}
// GetAutoDisconnectTimeResponse contains the "out" args for the "GetAutoDisconnectTime" action.
type GetAutoDisconnectTimeResponse struct {
+ // NewAutoDisconnectTime relates to state variable AutoDisconnectTime.
NewAutoDisconnectTime pkg2.UI4
}
@@ -195,7 +238,9 @@ type GetConnectionTypeInfoRequest struct{}
// GetConnectionTypeInfoResponse contains the "out" args for the "GetConnectionTypeInfo" action.
type GetConnectionTypeInfoResponse struct {
- NewConnectionType string
+ // NewConnectionType relates to state variable ConnectionType.
+ NewConnectionType string
+ // NewPossibleConnectionTypes relates to state variable PossibleConnectionTypes (7 standard allowed values).
NewPossibleConnectionTypes string
}
@@ -226,6 +271,7 @@ type GetExternalIPAddressRequest struct{}
// GetExternalIPAddressResponse contains the "out" args for the "GetExternalIPAddress" action.
type GetExternalIPAddressResponse struct {
+ // NewExternalIPAddress relates to state variable ExternalIPAddress.
NewExternalIPAddress string
}
@@ -253,19 +299,28 @@ func (a *GetGenericPortMappingEntry) RefResponse() any { return &a.Response }
// GetGenericPortMappingEntryRequest contains the "in" args for the "GetGenericPortMappingEntry" action.
type GetGenericPortMappingEntryRequest struct {
+ // NewPortMappingIndex relates to state variable PortMappingNumberOfEntries.
NewPortMappingIndex pkg2.UI2
}
// GetGenericPortMappingEntryResponse contains the "out" args for the "GetGenericPortMappingEntry" action.
type GetGenericPortMappingEntryResponse struct {
- NewRemoteHost string
- NewExternalPort pkg2.UI2
- NewProtocol string
- NewInternalPort pkg2.UI2
- NewInternalClient string
- NewEnabled pkg2.Boolean
+ // NewRemoteHost relates to state variable RemoteHost.
+ NewRemoteHost string
+ // NewExternalPort relates to state variable ExternalPort.
+ NewExternalPort pkg2.UI2
+ // NewProtocol relates to state variable PortMappingProtocol (2 standard allowed values).
+ NewProtocol string
+ // NewInternalPort relates to state variable InternalPort.
+ NewInternalPort pkg2.UI2
+ // NewInternalClient relates to state variable InternalClient.
+ NewInternalClient string
+ // NewEnabled relates to state variable PortMappingEnabled.
+ NewEnabled pkg2.Boolean
+ // NewPortMappingDescription relates to state variable PortMappingDescription.
NewPortMappingDescription string
- NewLeaseDuration pkg2.UI4
+ // NewLeaseDuration relates to state variable PortMappingLeaseDuration.
+ NewLeaseDuration pkg2.UI4
}
// GetIdleDisconnectTime provides request and response for the action.
@@ -295,6 +350,7 @@ type GetIdleDisconnectTimeRequest struct{}
// GetIdleDisconnectTimeResponse contains the "out" args for the "GetIdleDisconnectTime" action.
type GetIdleDisconnectTimeResponse struct {
+ // NewIdleDisconnectTime relates to state variable IdleDisconnectTime.
NewIdleDisconnectTime pkg2.UI4
}
@@ -325,7 +381,9 @@ type GetLinkLayerMaxBitRatesRequest struct{}
// GetLinkLayerMaxBitRatesResponse contains the "out" args for the "GetLinkLayerMaxBitRates" action.
type GetLinkLayerMaxBitRatesResponse struct {
- NewUpstreamMaxBitRate pkg2.UI4
+ // NewUpstreamMaxBitRate relates to state variable UpstreamMaxBitRate.
+ NewUpstreamMaxBitRate pkg2.UI4
+ // NewDownstreamMaxBitRate relates to state variable DownstreamMaxBitRate.
NewDownstreamMaxBitRate pkg2.UI4
}
@@ -356,8 +414,10 @@ type GetNATRSIPStatusRequest struct{}
// GetNATRSIPStatusResponse contains the "out" args for the "GetNATRSIPStatus" action.
type GetNATRSIPStatusResponse struct {
+ // NewRSIPAvailable relates to state variable RSIPAvailable.
NewRSIPAvailable pkg2.Boolean
- NewNATEnabled pkg2.Boolean
+ // NewNATEnabled relates to state variable NATEnabled.
+ NewNATEnabled pkg2.Boolean
}
// GetPPPAuthenticationProtocol provides request and response for the action.
@@ -387,6 +447,7 @@ type GetPPPAuthenticationProtocolRequest struct{}
// GetPPPAuthenticationProtocolResponse contains the "out" args for the "GetPPPAuthenticationProtocol" action.
type GetPPPAuthenticationProtocolResponse struct {
+ // NewPPPAuthenticationProtocol relates to state variable PPPAuthenticationProtocol.
NewPPPAuthenticationProtocol string
}
@@ -417,6 +478,7 @@ type GetPPPCompressionProtocolRequest struct{}
// GetPPPCompressionProtocolResponse contains the "out" args for the "GetPPPCompressionProtocol" action.
type GetPPPCompressionProtocolResponse struct {
+ // NewPPPCompressionProtocol relates to state variable PPPCompressionProtocol.
NewPPPCompressionProtocol string
}
@@ -447,6 +509,7 @@ type GetPPPEncryptionProtocolRequest struct{}
// GetPPPEncryptionProtocolResponse contains the "out" args for the "GetPPPEncryptionProtocol" action.
type GetPPPEncryptionProtocolResponse struct {
+ // NewPPPEncryptionProtocol relates to state variable PPPEncryptionProtocol.
NewPPPEncryptionProtocol string
}
@@ -477,6 +540,7 @@ type GetPasswordRequest struct{}
// GetPasswordResponse contains the "out" args for the "GetPassword" action.
type GetPasswordResponse struct {
+ // NewPassword relates to state variable Password.
NewPassword string
}
@@ -504,18 +568,26 @@ func (a *GetSpecificPortMappingEntry) RefResponse() any { return &a.Response }
// GetSpecificPortMappingEntryRequest contains the "in" args for the "GetSpecificPortMappingEntry" action.
type GetSpecificPortMappingEntryRequest struct {
- NewRemoteHost string
+ // NewRemoteHost relates to state variable RemoteHost.
+ NewRemoteHost string
+ // NewExternalPort relates to state variable ExternalPort.
NewExternalPort pkg2.UI2
- NewProtocol string
+ // NewProtocol relates to state variable PortMappingProtocol (2 standard allowed values).
+ NewProtocol string
}
// GetSpecificPortMappingEntryResponse contains the "out" args for the "GetSpecificPortMappingEntry" action.
type GetSpecificPortMappingEntryResponse struct {
- NewInternalPort pkg2.UI2
- NewInternalClient string
- NewEnabled pkg2.Boolean
+ // NewInternalPort relates to state variable InternalPort.
+ NewInternalPort pkg2.UI2
+ // NewInternalClient relates to state variable InternalClient.
+ NewInternalClient string
+ // NewEnabled relates to state variable PortMappingEnabled.
+ NewEnabled pkg2.Boolean
+ // NewPortMappingDescription relates to state variable PortMappingDescription.
NewPortMappingDescription string
- NewLeaseDuration pkg2.UI4
+ // NewLeaseDuration relates to state variable PortMappingLeaseDuration.
+ NewLeaseDuration pkg2.UI4
}
// GetStatusInfo provides request and response for the action.
@@ -545,9 +617,12 @@ type GetStatusInfoRequest struct{}
// GetStatusInfoResponse contains the "out" args for the "GetStatusInfo" action.
type GetStatusInfoResponse struct {
- NewConnectionStatus string
+ // NewConnectionStatus relates to state variable ConnectionStatus (3 standard allowed values).
+ NewConnectionStatus string
+ // NewLastConnectionError relates to state variable LastConnectionError (1 standard allowed values).
NewLastConnectionError string
- NewUptime pkg2.UI4
+ // NewUptime relates to state variable Uptime.
+ NewUptime pkg2.UI4
}
// GetUserName provides request and response for the action.
@@ -577,6 +652,7 @@ type GetUserNameRequest struct{}
// GetUserNameResponse contains the "out" args for the "GetUserName" action.
type GetUserNameResponse struct {
+ // NewUserName relates to state variable UserName.
NewUserName string
}
@@ -607,6 +683,7 @@ type GetWarnDisconnectDelayRequest struct{}
// GetWarnDisconnectDelayResponse contains the "out" args for the "GetWarnDisconnectDelay" action.
type GetWarnDisconnectDelayResponse struct {
+ // NewWarnDisconnectDelay relates to state variable WarnDisconnectDelay.
NewWarnDisconnectDelay pkg2.UI4
}
@@ -690,6 +767,7 @@ func (a *SetAutoDisconnectTime) RefResponse() any { return &a.Response }
// SetAutoDisconnectTimeRequest contains the "in" args for the "SetAutoDisconnectTime" action.
type SetAutoDisconnectTimeRequest struct {
+ // NewAutoDisconnectTime relates to state variable AutoDisconnectTime.
NewAutoDisconnectTime pkg2.UI4
}
@@ -720,6 +798,7 @@ func (a *SetConnectionType) RefResponse() any { return &a.Response }
// SetConnectionTypeRequest contains the "in" args for the "SetConnectionType" action.
type SetConnectionTypeRequest struct {
+ // NewConnectionType relates to state variable ConnectionType.
NewConnectionType string
}
@@ -750,6 +829,7 @@ func (a *SetIdleDisconnectTime) RefResponse() any { return &a.Response }
// SetIdleDisconnectTimeRequest contains the "in" args for the "SetIdleDisconnectTime" action.
type SetIdleDisconnectTimeRequest struct {
+ // NewIdleDisconnectTime relates to state variable IdleDisconnectTime.
NewIdleDisconnectTime pkg2.UI4
}
@@ -780,6 +860,7 @@ func (a *SetWarnDisconnectDelay) RefResponse() any { return &a.Response }
// SetWarnDisconnectDelayRequest contains the "in" args for the "SetWarnDisconnectDelay" action.
type SetWarnDisconnectDelayRequest struct {
+ // NewWarnDisconnectDelay relates to state variable WarnDisconnectDelay.
NewWarnDisconnectDelay pkg2.UI4
}
diff --git a/v2alpha/srv/srv.gotemplate b/v2alpha/srv/srv.gotemplate
index 73c307d..479be06 100644
--- a/v2alpha/srv/srv.gotemplate
+++ b/v2alpha/srv/srv.gotemplate
@@ -1,5 +1,6 @@
{{define "service"}}
{{- $Imps := .Imps -}}
+{{- $Types := .Types -}}
// Package {{.Manifest.Package}} provides types for the {{quote .Manifest.ServiceType}} service.
{{- with .Manifest.DocumentURL}}
//
@@ -13,15 +14,28 @@ import (
{{- end}}
)
+{{range .Types.StringVarDefs}}
+{{- $Name := .Name}}
+{{- with .AllowedValues}}
+// Allowed values for state variable {{$Name}}.
+const (
+{{- range .}}
+ {{$Name}}_{{.}} = "{{.}}"
+{{- end}}
+)
+{{- end}}
+{{- end}}
+
const ServiceType = {{quote .Manifest.ServiceType}}
{{range .SCPD.SortedActions}}
-{{- template "action" args "Action" . "Imps" $Imps}}
+{{- template "action" args "Action" . "Imps" $Imps "Types" $Types}}
{{end}}
{{- end}}
{{define "action"}}
{{- $Imps := .Imps}}
-{{- $soapActionType := index $Imps.TypeByName "SOAPActionInterface"}}
+{{- $Types := .Types}}
+{{- $soapActionType := index $Types.TypeByName "SOAPActionInterface"}}
// {{.Action.Name}} provides request and response for the action.
//
// ServiceType implements {{$soapActionType.AbsRef}}, self-describing the SOAP action.
@@ -43,18 +57,23 @@ func (a *{{.Action.Name}}) RefResponse() any { return &a.Response }
// {{.Action.Name}}Request contains the "in" args for the {{quote .Action.Name}} action.
type {{.Action.Name}}Request struct
-{{- template "args" args "Args" .Action.InArgs "Imps" $Imps}}
+{{- template "args" args "Args" .Action.InArgs "Imps" $Imps "Types" $Types}}
// {{.Action.Name}}Response contains the "out" args for the {{quote .Action.Name}} action.
type {{.Action.Name}}Response struct
-{{- template "args" args "Args" .Action.OutArgs "Imps" $Imps}}
+{{- template "args" args "Args" .Action.OutArgs "Imps" $Imps "Types" $Types}}
{{- end}}
{{define "args"}}
{{- $Imps := .Imps -}}
+{{- $Types := .Types -}}
{ {{- with .Args}}
{{- range .}}
-{{- $fieldType := index $Imps.TypeByName .RelatedStateVariable.DataType}}
+{{- $fieldType := index $Types.TypeByName .RelatedStateVariable.DataType}}
+ // {{.Name}} relates to state variable {{.RelatedStateVariable.Name}}
+{{- with .RelatedStateVariable.AllowedValues}}
+{{- ""}} ({{len .}} standard allowed values)
+{{- end }}.
{{.Name}} {{$fieldType.Ref}}
{{- end}}
{{end -}} }