Compare commits

...

16 Commits

Author SHA1 Message Date
11e9df2080 chore: upgrade dependencies 2024-07-31 19:06:38 +02:00
7a4ff9bdbd merge upstream and upgrade go to 1.21 2023-12-03 12:19:53 +01:00
e42f04b51d Merge remote-tracking branch 'origin/main' into feat/openhome 2023-12-03 12:13:54 +01:00
Andrew Dunham
00783e79ec httpu: add context.Context and related interface
This adds a new interface for httpu that supports a Context, and uses
that context to set a deadline/timeout and also cancel the request if
the context is canceled. Additionally, add a new method to the SSDP
package that takes a ClientInterfaceCtx.

Updates #55

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2023-08-29 19:12:39 +01:00
jybp
8ca2329ddb Use errors.As in test 2023-05-10 17:07:24 +01:00
jybp
c99b664f99 Fix test case 2023-05-10 17:07:24 +01:00
jybp
dc178c5d44 Fix faultcode+faultstring and get UPnPError details 2023-05-10 17:07:24 +01:00
John Beisley
15a204aa25 chore: gofmt. 2023-03-09 18:23:30 +00:00
John Beisley
e5bb4e5154 Include allowed string values in generated services. 2023-03-09 18:23:18 +00:00
John Beisley
1270e56d5f Fix error naming lint in srvdesc. 2023-03-09 17:08:45 +00:00
John Beisley
51ba21d432 Fix naming of soap/client.HTTPClient. 2023-03-09 16:35:15 +00:00
John Beisley
fe0b17f589 Introduce SOAPError type. 2023-03-09 16:34:46 +00:00
John Beisley
8e5cccc9ac Fix trivial lints in goupnp2srvgen. 2023-03-09 15:42:29 +00:00
John Beisley
d2cb593349 Include URL to upnpresources.zip in flag help. 2023-03-09 15:40:33 +00:00
Steve Hellwege
9278656124 Allow http.Client used in discovery to be modified (typically for security reasons)
[why]
The importing application may have some specific security requrirements that necessitate
a change to the http.Client or http.Transport used when fetching the xml from the UPnp server.
For example, the importing application may want to restrict localhost calls which could be
made by an attack server on the local network.

[how]
Create a global HTTPClient which defaults to http.DefaultClient.  This allows the importing
application to modify this global if it wishes to make changes to the http.Client/http.Transport
used when fetching the xml from the UPnP server.
2023-02-12 17:29:00 +00:00
Kz Ho
62bd5c75d8 Add sync.Pool to reuse packet conn buf object 2023-01-18 08:53:09 +00:00
25 changed files with 693 additions and 151 deletions

4
go.mod
View File

@ -1,5 +1,5 @@
module git.cyrilix.bzh/cyrilix/goupnp
go 1.19
go 1.22
require golang.org/x/sync v0.1.0
require golang.org/x/sync v0.7.0

4
go.sum
View File

@ -1,2 +1,2 @@
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=

View File

@ -1,4 +1,6 @@
go 1.18
go 1.22
toolchain go1.22.5
use (
.

6
go.work.sum Normal file
View File

@ -0,0 +1,6 @@
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=

View File

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

View File

@ -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,9 +143,27 @@ func (httpu *HTTPUClient) Do(
if err != nil {
return nil, err
}
if err = httpu.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
// 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++ {

View File

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

View File

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

View File

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

View File

@ -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"`
}

View File

@ -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:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring>UPnPError</faultstring>
<detail>
<UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
<errorCode>725</errorCode>
<errorDescription>OnlyPermanentLeasesSupported</errorDescription>
</UPnPError>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>`
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), `
<UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
<errorCode>725</errorCode>
<errorDescription>OnlyPermanentLeasesSupported</errorDescription>
</UPnPError>
`) {
t.Fatalf("unexpected Detail.Raw, got:\n%s", string(soapErr.Detail.Raw))
}
}
func TestEscapeXMLText(t *testing.T) {
t.Parallel()

View File

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

View File

@ -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{
return &types{
TypeByName: typeByName,
ImportLines: importLines,
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
}

View File

@ -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,
AllowedValues: xmlSV.AllowedValues,
}, nil
}

View File

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

View File

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

View File

@ -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
httpClient HTTPClient
endpointURL string
maxErrorResponseBytes int
}
// 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 fmt.Errorf(
"parsing response body (%s%q): %w",
return &SOAPError{
description: fmt.Sprintf(
"parsing SOAP response from HTTP body (%s%q)",
truncMessage, buf.Bytes()[:dispLen],
err,
)
),
cause: err,
}
}
return nil

View File

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

View File

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

View File

@ -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,13 +63,21 @@ func (a *AddPortMapping) RefResponse() any { return &a.Response }
// AddPortMappingRequest contains the "in" args for the "AddPortMapping" action.
type AddPortMappingRequest struct {
// 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 relates to state variable PortMappingLeaseDuration.
NewLeaseDuration pkg2.UI4
}
@ -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,8 +141,11 @@ func (a *DeletePortMapping) RefResponse() any { return &a.Response }
// DeletePortMappingRequest contains the "in" args for the "DeletePortMapping" action.
type DeletePortMappingRequest struct {
// 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
}
@ -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 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,18 +299,27 @@ 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 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 relates to state variable PortMappingLeaseDuration.
NewLeaseDuration pkg2.UI4
}
@ -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 relates to state variable UpstreamMaxBitRate.
NewUpstreamMaxBitRate pkg2.UI4
// NewDownstreamMaxBitRate relates to state variable DownstreamMaxBitRate.
NewDownstreamMaxBitRate pkg2.UI4
}
@ -356,7 +414,9 @@ 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 relates to state variable NATEnabled.
NewNATEnabled pkg2.Boolean
}
@ -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,17 +568,25 @@ func (a *GetSpecificPortMappingEntry) RefResponse() any { return &a.Response }
// GetSpecificPortMappingEntryRequest contains the "in" args for the "GetSpecificPortMappingEntry" action.
type GetSpecificPortMappingEntryRequest struct {
// 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
}
// GetSpecificPortMappingEntryResponse contains the "out" args for the "GetSpecificPortMappingEntry" action.
type GetSpecificPortMappingEntryResponse struct {
// 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 relates to state variable PortMappingLeaseDuration.
NewLeaseDuration pkg2.UI4
}
@ -545,8 +617,11 @@ type GetStatusInfoRequest struct{}
// GetStatusInfoResponse contains the "out" args for the "GetStatusInfo" action.
type GetStatusInfoResponse struct {
// 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 relates to state variable Uptime.
NewUptime pkg2.UI4
}
@ -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
}

View File

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

View File

@ -20,7 +20,7 @@ type token struct{}
// A zero Group is valid, has no limit on the number of active goroutines,
// and does not cancel on error.
type Group struct {
cancel func()
cancel func(error)
wg sync.WaitGroup
@ -43,7 +43,7 @@ func (g *Group) done() {
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := withCancelCause(ctx)
return &Group{cancel: cancel}, ctx
}
@ -52,7 +52,7 @@ func WithContext(ctx context.Context) (*Group, context.Context) {
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
g.cancel(g.err)
}
return g.err
}
@ -76,7 +76,7 @@ func (g *Group) Go(f func() error) {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
g.cancel(g.err)
}
})
}
@ -105,7 +105,7 @@ func (g *Group) TryGo(f func() error) bool {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
g.cancel(g.err)
}
})
}

13
vendor/golang.org/x/sync/errgroup/go120.go generated vendored Normal file
View File

@ -0,0 +1,13 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.20
package errgroup
import "context"
func withCancelCause(parent context.Context) (context.Context, func(error)) {
return context.WithCancelCause(parent)
}

14
vendor/golang.org/x/sync/errgroup/pre_go120.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.20
package errgroup
import "context"
func withCancelCause(parent context.Context) (context.Context, func(error)) {
ctx, cancel := context.WithCancel(parent)
return ctx, func(error) { cancel() }
}

4
vendor/modules.txt vendored
View File

@ -1,3 +1,3 @@
# golang.org/x/sync v0.1.0
## explicit
# golang.org/x/sync v0.5.0
## explicit; go 1.18
golang.org/x/sync/errgroup