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 -}} }