diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..9155813 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +--- +name: 'Automatically relase patch versions.' + +on: + schedule: # Run every day at 12:00 UTC + - cron: '0 12 * * *' + workflow_dispatch: + +jobs: + release: + uses: xmidt-org/shared-go/.github/workflows/auto-releaser.yml@12a58ef01ff3da83567e930742524d5a215b90a2 # v4.4.17 + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b225147..1305a29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ on: jobs: ci: - uses: xmidt-org/shared-go/.github/workflows/ci.yml@f15dd81338419cfdec19a9110510cf9bf3182ca6 # v4.4.1 + uses: xmidt-org/shared-go/.github/workflows/ci.yml@12a58ef01ff3da83567e930742524d5a215b90a2 # v4.4.17 with: release-type: program release-main-package: ./cmd/xmidt-agent diff --git a/.goreleaser.yml b/.goreleaser.yml index 75745dc..8ef756f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,7 @@ --- project_name: xmidt-agent +version: 2 changelog: use: github @@ -71,6 +72,63 @@ archives: format: zip name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' +nfpms: + - + id: rdk + file_name_template: '{{ .PackageName }}_{{ .Version }}_rdk_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' + vendor: xmidt-org + homepage: https://github.com/xmidt-org/xmidt-agent + license: Apache-2.0 + maintainer: see_maintainers_file@example.com + description: The client agent for the Xmidt service. + + formats: + - ipk + + dependencies: + - systemd + + contents: + # systemd service file + - src: .release/ipk/xmidt-agent.service + dst: /lib/systemd/system/xmidt-agent.service + + # base configuration file + - src: .release/ipk/config.yml + dst: /etc/xmidt-agent/01-config.yml + + scripts: + preinstall: .release/ipk/preinstall.sh + postinstall: .release/ipk/postinstall.sh + + ipk: + fields: + Bugs: https://github.com/xmidt-org/xmidt-agent/issues + + - + id: openwrt + file_name_template: '{{ .PackageName }}_{{ .Version }}_openwrt_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' + vendor: xmidt-org + homepage: https://github.com/xmidt-org/xmidt-agent + license: Apache-2.0 + maintainer: see_maintainers_file@example.com + description: The client agent for the Xmidt service. + + formats: + - ipk + + dependencies: + - systemd + + contents: + # base configuration file + - src: .release/ipk/config.yml + dst: /etc/xmidt-agent/01-config.yml + + ipk: + fields: + Bugs: https://github.com/xmidt-org/xmidt-agent/issues + source: enabled: true name_template: '{{ .ProjectName }}_{{ .Version }}_src' diff --git a/.release/docker/config/config.yml b/.release/docker/config/config.yml index 88d2323..80ea265 100644 --- a/.release/docker/config/config.yml +++ b/.release/docker/config/config.yml @@ -3,4 +3,4 @@ mock_tr_181: enabled: true file_path: "/mock_tr181.json" - service_name: "mock_config" + service_name: "config" diff --git a/.release/docker/config/example-config.yaml b/.release/docker/config/example-config.yaml deleted file mode 100644 index 2268080..0000000 --- a/.release/docker/config/example-config.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC -# SPDX-License-Identifier: Apache-2.0 - -websocket: - url_path: api/v2/device - back_up_url: http://localhost:8080 -xmidt_credentials: - # if url is empty, there is no auth - url: http://localhost:6501/issue - file_name: outputJwt.txt - file_permissions: 0777 - http_client: - tls: - insecure_skip_verify: true - certificates: - - certificate_file: certs/cert.pem - key_file: certs/key.pem - min_version: 771 # 0x0303, the TLS 1.2 version uint16 -identity: - device_id: mac:4ca161000109 - serial_number: 1800deadbeef - hardware_model: fooModel - hardware_manufacturer: barManufacturer - firmware_version: v0.0.1 - partner_id: foobar -xmidt_service: - backoff: - max_delay: 600s - min_delay: 7s - # config for an optional server that will redirect the device to a websocket server - jwt_txt_redirector: - allowed_algorithms: ["RS256"] - pems: - - | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkZeQUMqDkMu/dQgTcycJ - /rrgqfKQkoYFTMO7GnK87/OWiftHmtDJYPgktlcHheOPfkGln5ay1WJSitmtWNaH - +RvBFK/ZsXpIuGm7R6wdSc7e6g9fTaSAfNX/+a8VxHUo58AinXxcq4LnHbuaGjEz - jw77TLuZGyUuHiany8O8tc+DbnYKvRquScsccI6z/QwZKFtXUjJZ91hJ97zC8o7N - Ae7n/Jg+Bs0uz9c1/bf/Jqbu6OidFbCr2FN42UupuAZ8DiPp2fWD5Q9qmp1ADk+V - +TeZPxTCq/WB4dzSCd5v/FvFmO8tH6Ptkltij4pke7Dsi80TVRlcMDXAWxSFXOQV - qwIDAQAB - -----END PUBLIC KEY----- - timeout: 10s - url: https://localhost:8080 -operational_state: - last_reboot_reason: sleepy - boot_time: "2024-02-28T01:04:27Z" -storage: - temporary: ~/local-rdk-testing/temporary - durable: ~/local-rdk-testing/durable -mock_tr_181: - enabled: true - file_path: /mock_tr181.json diff --git a/.release/ipk/config.yml b/.release/ipk/config.yml new file mode 100644 index 0000000..f16a386 --- /dev/null +++ b/.release/ipk/config.yml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Put any configuration values that should always be present here. +# +# Note: +# These values should be generic and apply to all consumers (openwrt, rdk, etc) +--- diff --git a/.release/ipk/postinstall.sh b/.release/ipk/postinstall.sh new file mode 100755 index 0000000..48bffb6 --- /dev/null +++ b/.release/ipk/postinstall.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +systemctl preset xmidt-agent.service >/dev/null 2>&1 diff --git a/.release/ipk/preinstall.sh b/.release/ipk/preinstall.sh new file mode 100755 index 0000000..4623429 --- /dev/null +++ b/.release/ipk/preinstall.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +getent group xmidt-agent > /dev/null || groupadd -r xmidt-agent +getent passwd xmidt-agent > /dev/null || \ + useradd \ + -d /var/run/xmidt-agent \ + -r \ + -g xmidt-agent \ + -s /sbin/nologin \ + -c "Xmidt-Agent Client" \ + xmidt-agent \ No newline at end of file diff --git a/.release/ipk/xmidt-agent.service b/.release/ipk/xmidt-agent.service new file mode 100644 index 0000000..31ac404 --- /dev/null +++ b/.release/ipk/xmidt-agent.service @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Description=The client agent for the Xmidt service. +After=network.target remote-fs.target nss-lookup.target + +[Service] +User=xmidt-agent +Group=xmidt-agent +SyslogIdentifier=xmidt-agent +PIDFile=/run/xmidt-agent.pid +ExecStartPre=/usr/bin/rm -f /run/xmidt-agent.pid +ExecStart=/usr/bin/xmidt-agent +Type=simple +ExecReload=/bin/kill -s HUP $MAINPID +KillMode=process +PrivateTmp=true +Restart=always + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/cmd/xmidt-agent/config.go b/cmd/xmidt-agent/config.go index 415e679..a0f7e07 100644 --- a/cmd/xmidt-agent/config.go +++ b/cmd/xmidt-agent/config.go @@ -275,6 +275,15 @@ func provideConfig(cli *CLI) (*goschtalt.Config, error) { return nil, err } + if cli.Default != "" { + err := os.WriteFile("./"+cli.Default, defaultConfigFile, 0644) // nolint: gosec + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + os.Exit(0) + } + if cli.Show { // handleCLIShow handles the -s/--show option where the configuration is // shown, then the program is exited. diff --git a/cmd/xmidt-agent/default-config.yaml b/cmd/xmidt-agent/default-config.yaml index e2b258a..080e92a 100644 --- a/cmd/xmidt-agent/default-config.yaml +++ b/cmd/xmidt-agent/default-config.yaml @@ -102,10 +102,10 @@ logger: level_key: L name_key: N caller_key: C - function_key: "zapcore.OmitKey" + function_key: "" message_key: M stacktrace_key: S - line_ending: "zapcore.DefaultLineEnding" + line_ending: "\n" encode_level: capital encode_time: RFC3339Nano encode_duration: string @@ -121,7 +121,7 @@ storage: # temporary: "~/local-rdk-testing/temporary" # durable: "~/local-rdk-testing/durable" mock_tr_181: - enabled: true + enabled: false file_path: "mock_tr181.json" service_name: "mock_config" xmidt_agent_crud: diff --git a/cmd/xmidt-agent/main.go b/cmd/xmidt-agent/main.go index 89be17c..31df927 100644 --- a/cmd/xmidt-agent/main.go +++ b/cmd/xmidt-agent/main.go @@ -38,10 +38,11 @@ var ( // CLI is the structure that is used to capture the command line arguments. type CLI struct { - Dev bool `optional:"" short:"d" help:"Run in development mode."` - Show bool `optional:"" short:"s" help:"Show the configuration and exit."` - Graph string `optional:"" short:"g" help:"Output the dependency graph to the specified file."` - Files []string `optional:"" short:"f" help:"Specific configuration files or directories."` + Dev bool `optional:"" short:"d" help:"Run in development mode."` + Show bool `optional:"" short:"s" help:"Show the configuration and exit."` + Default string `optional:"" help:"Output the default configuration file as the specified file."` + Graph string `optional:"" short:"g" help:"Output the dependency graph to the specified file."` + Files []string `optional:"" short:"f" help:"Specific configuration files or directories."` } type LifeCycleIn struct { diff --git a/cmd/xmidt-agent/xmidt_agent.yaml b/cmd/xmidt-agent/xmidt_agent.yaml deleted file mode 100644 index 67fcd3f..0000000 --- a/cmd/xmidt-agent/xmidt_agent.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC -# SPDX-License-Identifier: Apache-2.0 ---- -websocket: - url_path: api/v2/device -identity: - device_id: mac:00deadbeef00 - serial_number: 1800deadbeef - hardware_model: fooModel - hardware_manufacturer: barManufacturer - firmware_version: v0.0.1 - partner_id: foobar -operational_state: - last_reboot_reason: sleepy - boot_time: "2024-02-28T01:04:27Z" -xmidt_service: - url: ${MAIL} -# Optional (Examples) -# xmidt_credentials: -# url: ${TOKEN_URL} -# file_name: crt.pem -# file_permissions: 0777 -# http_client: -# tls: -# insecure_skip_verify: true -# certificates: -# - certificate_file: crt.pem -# key_file: key.pem -# min_version: 771 # 0x0303, the TLS 1.2 version uint16 -# storage: -# temporary: ~/local-rdk-testing/temporary -# durable: ~/local-rdk-testing/durable -# mock_tr_181: -# enabled: true -# file_path: xmidt-agent/internal/wrphandlers/mocktr181/mock_tr181.json -# externals: -# - -# file: ~/local-rdk-testing/durable/external.txt -# as: properties -# remap: -# - from: Device.X_RDK_WebPA_Server.URL -# to: XMIDT_URL -# - from: Device.X_RDK_WebPA_DNSText.URL -# to: DNS_URL -# - from: Device.X_RDK_WebPA_TokenServer.URL -# to: TOKEN_URL diff --git a/go.mod b/go.mod index ef4bc29..1816770 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,12 @@ require ( github.com/stretchr/testify v1.9.0 github.com/ugorji/go/codec v1.2.12 github.com/xmidt-org/arrange v0.5.0 - github.com/xmidt-org/eventor v1.0.0 + github.com/xmidt-org/eventor v1.0.11 github.com/xmidt-org/retry v0.0.3 github.com/xmidt-org/sallust v0.2.2 github.com/xmidt-org/wrp-go/v3 v3.5.2 go.nanomsg.org/mangos/v3 v3.4.2 - go.uber.org/fx v1.22.0 + go.uber.org/fx v1.22.1 go.uber.org/zap v1.27.0 gopkg.in/dealancer/validate.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index 58fee27..015fc06 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xmidt-org/arrange v0.5.0 h1:ajkVHkr7dXnfCYm/6eafWoOab+6A3b2jEHQO0IdIIb0= github.com/xmidt-org/arrange v0.5.0/go.mod h1:PoZB9lR49ma0osydQbaWpNeA3XPoLkjP5RYUoOw8wZU= -github.com/xmidt-org/eventor v1.0.0 h1:FiX4UsU4gztIcAn8UJAZwJjNleOeeDyBxuBS4nGeuik= -github.com/xmidt-org/eventor v1.0.0/go.mod h1:NpaRwPEiiaB5oEdFI41o6Lf4iQHAVwCdtwKb3z7R8mY= +github.com/xmidt-org/eventor v1.0.11 h1:yBoF39jzxSBeDvMVJVVyL/g85UO8/g1GxLojMmxQj5E= +github.com/xmidt-org/eventor v1.0.11/go.mod h1:NpaRwPEiiaB5oEdFI41o6Lf4iQHAVwCdtwKb3z7R8mY= github.com/xmidt-org/httpaux v0.4.0 h1:cAL/MzIBpSsv4xZZeq/Eu1J5M3vfNe49xr41mP3COKU= github.com/xmidt-org/httpaux v0.4.0/go.mod h1:UypqZwuZV1nn8D6+K1JDb+im9IZrLNg/2oO/Bgiybxc= github.com/xmidt-org/retry v0.0.3 h1:wvmBnEEn1OKwSZaQtr1RZ2Vey8JIvP72mGTgR+3wPiM= @@ -81,8 +81,8 @@ go.nanomsg.org/mangos/v3 v3.4.2 h1:gHlopxjWvJcVCcUilQIsRQk9jdj6/HB7wrTiUN8Ki7Q= go.nanomsg.org/mangos/v3 v3.4.2/go.mod h1:8+hjBMQub6HvXmuGvIq6hf19uxGQIjCofmc62lbedLA= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.22.0 h1:pApUK7yL0OUHMd8vkunWSlLxZVFFk70jR2nKde8X2NM= -go.uber.org/fx v1.22.0/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= +go.uber.org/fx v1.22.1 h1:nvvln7mwyT5s1q201YE29V/BFrGor6vMiDNpU/78Mys= +go.uber.org/fx v1.22.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index 5ab7b4b..5d8ec4b 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "encoding/base64" "github.com/xmidt-org/wrp-go/v3" "github.com/xmidt-org/xmidt-agent/internal/net" ) @@ -114,7 +115,7 @@ func (c *MetadataProvider) Decorate(headers http.Header) error { return fmt.Errorf("error marshaling convey header: %w", err) } - headers.Set(HeaderName, string(headerBytes)) + headers.Set(HeaderName, base64.StdEncoding.EncodeToString(headerBytes)) return nil } diff --git a/internal/pubsub/pubsub.go b/internal/pubsub/pubsub.go index 5061ee1..4897e1c 100644 --- a/internal/pubsub/pubsub.go +++ b/internal/pubsub/pubsub.go @@ -148,7 +148,7 @@ func (ps *PubSub) subscribe(route string, h wrpkit.Handler) (CancelFunc, error) func (ps *PubSub) HandleWrp(msg wrp.Message) error { normalized, dest, err := ps.normalize(&msg) if err != nil { - return err + return errors.Join(err, wrpkit.ErrNotHandled) } // Unless the destination is this device, the message will be sent to the diff --git a/internal/websocket/ws.go b/internal/websocket/ws.go index b9b2669..de4af8d 100644 --- a/internal/websocket/ws.go +++ b/internal/websocket/ws.go @@ -228,7 +228,6 @@ func (ws *Websocket) run(ctx context.Context) { mode := ws.nextMode(ipv4) policy := ws.retryPolicyFactory.NewPolicy(ctx) - inactivityTimeout := time.After(ws.inactivityTimeout) for { var next time.Duration @@ -258,6 +257,7 @@ func (ws *Websocket) run(ctx context.Context) { // Store the connection so writing can take place. ws.m.Lock() ws.conn = conn + activity := make(chan struct{}) ws.conn.SetPingListener((func(ctx context.Context, b []byte) { if ctx.Err() != nil { return @@ -270,7 +270,9 @@ func (ws *Websocket) run(ctx context.Context) { }) }) - inactivityTimeout = time.After(ws.inactivityTimeout) + if len(activity) == 0 { + activity <- struct{}{} + } })) ws.conn.SetPongListener(func(ctx context.Context, b []byte) { if ctx.Err() != nil { @@ -289,22 +291,32 @@ func (ws *Websocket) run(ctx context.Context) { // Read loop for { var msg wrp.Message - ctx, cancel := context.WithTimeout(ctx, ws.inactivityTimeout) - typ, reader, err := ws.conn.Reader(ctx) - if errors.Is(err, context.DeadlineExceeded) { - select { - case <-inactivityTimeout: - // inactivityTimeout occurred, continue with ws.read()'s error handling (connection will be closed). - default: - // Ping was received during ws.conn.Reader(), i.e.: inactivityTimeout was reset. - // Reset inactivityTimeout again for the next ws.conn.Reader(). - inactivityTimeout = time.After(ws.inactivityTimeout) - cancel() - continue + ctx, cancel := context.WithCancelCause(ctx) + + // Monitor for activity. + go func() { + inactivityTimeout := time.After(ws.inactivityTimeout) + loop1: + for { + select { + case <-ctx.Done(): + break loop1 + case <-activity: + inactivityTimeout = time.After(ws.inactivityTimeout) + case <-inactivityTimeout: + // inactivityTimeout occurred, cancel the context. + cancel(context.DeadlineExceeded) + break loop1 + } } - } else if errors.Is(err, context.Canceled) { - // Parent context has been canceled. - cancel() + }() + + typ, reader, err := ws.conn.Reader(ctx) + ctxErr := context.Cause(ctx) + err = errors.Join(err, ctxErr) + // If ctxErr is context.Canceled then the parent context has been canceled. + if errors.Is(ctxErr, context.Canceled) { + cancel(nil) break } @@ -318,7 +330,7 @@ func (ws *Websocket) run(ctx context.Context) { } // Cancel ws.conn.Reader()'s context after wrp decoding. - cancel() + cancel(nil) if err != nil { ws.m.Lock() ws.conn = nil @@ -411,6 +423,15 @@ func (ws *Websocket) newHTTPClient(mode ipMode) (*http.Client, error) { return nil, err } + // update client redirect to send all headers on subsequent requests + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + // Copy headers from the first request to original requests + for key, value := range via[0].Header { + req.Header[key] = value + } + return nil + } + // Override config.NewClient()'s Transport and update it's DialContext with the provided mode. transport, err := config.Transport.NewTransport(config.TLS) if err != nil { diff --git a/internal/wrphandlers/auth/handler.go b/internal/wrphandlers/auth/handler.go index 331ed0d..89574ce 100644 --- a/internal/wrphandlers/auth/handler.go +++ b/internal/wrphandlers/auth/handler.go @@ -87,11 +87,11 @@ func (h Handler) HandleWrp(msg wrp.Message) error { response := msg response.Destination = msg.Source response.Source = h.source - response.ContentType = "text/plain" - response.Payload = []byte(fmt.Sprintf("Partner(s) '%s' not allowed. Allowed: '%s'", got, want)) + response.ContentType = "application/json" code := int64(statusCode) response.Status = &code + response.Payload = []byte(fmt.Sprintf(`{statusCode: %d, message:"Partner(s) '%s' not allowed. Allowed: '%s'"}`, code, got, want)) sendErr := h.egress.HandleWrp(response) diff --git a/internal/wrphandlers/missing/handler.go b/internal/wrphandlers/missing/handler.go index 48d6d9e..0072a37 100644 --- a/internal/wrphandlers/missing/handler.go +++ b/internal/wrphandlers/missing/handler.go @@ -70,10 +70,11 @@ func (h Handler) HandleWrp(msg wrp.Message) error { response := msg response.Destination = msg.Source response.Source = h.source - response.Payload = nil + response.ContentType = "application/json" code := int64(statusCode) response.Status = &code + response.Payload = []byte(fmt.Sprintf("{statusCode: %d}", code)) sendErr := h.egress.HandleWrp(response) diff --git a/internal/wrphandlers/mocktr181/handler.go b/internal/wrphandlers/mocktr181/handler.go index 22655fe..ab593a0 100644 --- a/internal/wrphandlers/mocktr181/handler.go +++ b/internal/wrphandlers/mocktr181/handler.go @@ -60,6 +60,7 @@ type Tr181Payload struct { Command string `json:"command"` Names []string `json:"names"` Parameters []Parameter `json:"parameters"` + StatusCode int `json:"statusCode"` } type Parameters struct { @@ -71,6 +72,8 @@ type Parameter struct { Value string `json:"value"` DataType int `json:"dataType"` Attributes map[string]interface{} `json:"attributes"` + Message string `json:"message"` + Count int `json:"parameterCount"` } // New creates a new instance of the Handler struct. The parameter egress is @@ -126,23 +129,27 @@ func (h Handler) HandleWrp(msg wrp.Message) error { switch command { case "GET": - statusCode, payloadResponse, err = h.get(payload.Names) + statusCode, payloadResponse, err = h.get(payload) if err != nil { return err } case "SET": - statusCode = h.set(payload.Parameters) + statusCode, payloadResponse, err = h.set(payload) + if err != nil { + return err + } default: // currently only get and set are implemented for existing mocktr181 - statusCode = http.StatusOK + statusCode = 520 + payloadResponse = []byte(fmt.Sprintf(`{"message": "command %s is not support", "statusCode": %d}`, command, statusCode)) } response := msg response.Destination = msg.Source response.Source = h.source - response.ContentType = "text/plain" + response.ContentType = "application/json" response.Payload = payloadResponse response.Status = &statusCode @@ -152,49 +159,155 @@ func (h Handler) HandleWrp(msg wrp.Message) error { return err } -func (h Handler) get(names []string) (int64, []byte, error) { - result := Tr181Payload{} - statusCode := int64(http.StatusOK) +func (h Handler) get(tr181 *Tr181Payload) (int64, []byte, error) { + result := Tr181Payload{ + Command: tr181.Command, + Names: tr181.Names, + StatusCode: http.StatusOK, + } - for _, name := range names { + var ( + failedNames []string + readableParams []Parameter + ) + for _, name := range tr181.Names { + var found bool for _, mockParameter := range h.parameters { - if strings.HasPrefix(mockParameter.Name, name) { - result.Parameters = append(result.Parameters, Parameter{ + if name == "" { + continue + } + + if !strings.HasPrefix(mockParameter.Name, name) { + continue + } + + // Check whether mockParameter is readable. + if strings.Contains(mockParameter.Access, "r") { + found = true + readableParams = append(readableParams, Parameter{ Name: mockParameter.Name, Value: mockParameter.Value, DataType: mockParameter.DataType, Attributes: mockParameter.Attributes, + Message: "Success", + Count: 1, }) + continue + } + + // If the requested parameter is a wild card and is not readable, + // then continue and don't count it as a failure. + if name[len(name)-1] == '.' { + continue } + + // mockParameter is not readable. + failedNames = append(failedNames, mockParameter.Name) + } + + if !found { + // Requested parameter was not found. + failedNames = append(failedNames, name) } } + result.Parameters = readableParams + // Check if any parameters failed. + if len(failedNames) != 0 { + // If any names failed, then do not return any parameters that succeeded. + result.Parameters = []Parameter{{ + Message: fmt.Sprintf("Invalid parameter names: %s", failedNames), + }} + result.StatusCode = 520 + } + payload, err := json.Marshal(result) if err != nil { return http.StatusInternalServerError, payload, errors.Join(ErrInvalidResponsePayload, err) } - if len(result.Parameters) == 0 { - statusCode = int64(520) + return int64(result.StatusCode), payload, nil +} + +func (h Handler) set(tr181 *Tr181Payload) (int64, []byte, error) { + result := Tr181Payload{ + Command: tr181.Command, + Names: tr181.Names, + StatusCode: http.StatusAccepted, } - return statusCode, payload, nil -} + var ( + writableParams []*MockParameter + failedParams []Parameter + ) + // Check for any parameters that are not writable. + for _, parameter := range tr181.Parameters { + var found bool + for i := range h.parameters { + mockParameter := &h.parameters[i] + if mockParameter.Name != parameter.Name { + continue + } -func (h Handler) set(parameters []Parameter) int64 { - for _, parameter := range parameters { - for _, mockParameter := range h.parameters { - if strings.HasPrefix(mockParameter.Name, parameter.Name) { - if mockParameter.Access == "rw" { - mockParameter.Value = parameter.Value - mockParameter.DataType = parameter.DataType - mockParameter.Attributes = parameter.Attributes - } + // Check whether mockParameter is writable. + if strings.Contains(mockParameter.Access, "w") { + found = true + // Add mockParameter to the list of parameters to be updated. + writableParams = append(writableParams, mockParameter) + continue } + + // mockParameter is not writable. + failedParams = append(failedParams, Parameter{ + Name: mockParameter.Name, + Message: "Parameter is not writable", + }) + } + + if !found { + // Requested parameter was not found. + failedParams = append(failedParams, Parameter{ + Name: parameter.Name, + Message: "Invalid parameter name", + }) } } - return http.StatusAccepted + // Check if any parameters failed. + if len(failedParams) != 0 { + // If any parameter failed, then do not apply any changes to the parameters in writableParams. + writableParams = nil + result.Parameters = failedParams + result.StatusCode = 520 + } + + // If all the selected parameters are writable, then update the parameters. Otherwise, do nothing. + for _, parameter := range tr181.Parameters { + // writableParams will be nil if any parameters failed (i.e.: were not writable). + for _, mockParameter := range writableParams { + if mockParameter.Name != parameter.Name { + continue + } + + mockParameter.Value = parameter.Value + mockParameter.DataType = parameter.DataType + mockParameter.Attributes = parameter.Attributes + result.Parameters = append(result.Parameters, Parameter{ + Name: mockParameter.Name, + Value: mockParameter.Value, + DataType: mockParameter.DataType, + Attributes: mockParameter.Attributes, + Message: "Success", + }) + } + } + + payload, err := json.Marshal(result) + if err != nil { + return http.StatusInternalServerError, payload, errors.Join(ErrInvalidResponsePayload, err) + } + + return int64(result.StatusCode), payload, nil } func (h Handler) loadFile() ([]MockParameter, error) { diff --git a/internal/wrphandlers/mocktr181/handler_test.go b/internal/wrphandlers/mocktr181/handler_test.go index 18edae1..dbb0815 100644 --- a/internal/wrphandlers/mocktr181/handler_test.go +++ b/internal/wrphandlers/mocktr181/handler_test.go @@ -59,7 +59,7 @@ func TestHandler_HandleWrp(t *testing.T) { var result Tr181Payload err := json.Unmarshal(msg.Payload, &result) a.NoError(err) - a.Equal(0, len(result.Parameters)) + a.Equal(1, len(result.Parameters)) a.True(h.Enabled()) return nil }, @@ -88,7 +88,22 @@ func TestHandler_HandleWrp(t *testing.T) { Payload: []byte("{\"command\":\"SET\",\"parameters\":[{\"name\":\"Device.Bridging.MaxBridgeEntries\",\"dataType\":0,\"value\":\"anothername\",\"attributes\":{\"notify\":0}}]}"), }, validate: func(a *assert.Assertions, msg wrp.Message, h *Handler) error { - a.Equal(int64(http.StatusAccepted), *msg.Status) + a.Equal(int64(520), *msg.Status) + a.True(h.Enabled()) + + return nil + }, + }, { + description: "unknown command", + egressCallCount: 1, + msg: wrp.Message{ + Type: wrp.SimpleEventMessageType, + Source: "dns:tr1d1um.example.com/service/ignored", + Destination: "event:event_1/ignored", + Payload: []byte("{\"command\":\"FOOBAR\",\"parameters\":[{\"name\":\"Device.Bridging.MaxBridgeEntries\",\"dataType\":0,\"value\":\"anothername\",\"attributes\":{\"notify\":0}}]}"), + }, + validate: func(a *assert.Assertions, msg wrp.Message, h *Handler) error { + a.Equal(int64(520), *msg.Status) a.True(h.Enabled()) return nil diff --git a/internal/wrphandlers/xmidt_agent_crud/handler.go b/internal/wrphandlers/xmidt_agent_crud/handler.go index 8cb10c9..8c71101 100644 --- a/internal/wrphandlers/xmidt_agent_crud/handler.go +++ b/internal/wrphandlers/xmidt_agent_crud/handler.go @@ -5,6 +5,7 @@ package xmidt_agent_crud import ( "encoding/json" + "fmt" "net/http" "time" @@ -36,31 +37,35 @@ func New(egress wrpkit.Handler, source string, logLevel loglevel.LogLevel) (*Han } func (h *Handler) HandleWrp(msg wrp.Message) error { + response := msg + response.Destination = msg.Source + response.Source = h.source + response.ContentType = "application/json" payload := make(map[string]string) err := json.Unmarshal(msg.Payload, &payload) - if err != nil { // do we still want to return a response here? - return err // this would be sent to the eventor error handler + if err != nil { + statusCode := int64(http.StatusInternalServerError) + response.Status = &statusCode + response.Payload = []byte(fmt.Sprintf(`{statusCode: %d, message: "%s"}`, statusCode, err.Error())) + return h.egress.HandleWrp(response) } - var payloadResponse []byte statusCode := int64(http.StatusBadRequest) + payloadResponse := []byte(fmt.Sprintf(`{statusCode: %d, message: "%s"}`, statusCode, "")) switch msg.Type { case wrp.UpdateMessageType: statusCode, err = h.update(msg.Path, payload) + payloadResponse = []byte(fmt.Sprintf(`{statusCode: %d, message: "%s"}`, statusCode, "")) if err != nil { - payloadResponse = []byte(err.Error()) + payloadResponse = []byte(fmt.Sprintf(`{statusCode: %d, message: "%s"}`, statusCode, err.Error())) } default: } - response := msg - response.Destination = msg.Source - response.Source = h.source - response.ContentType = "text/plain" response.Payload = payloadResponse response.Status = &statusCode