From 7cfb6e51e3e3e541265ca490485d7bf9cf1f3944 Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Wed, 15 May 2024 09:17:32 +0200 Subject: [PATCH] http-gateway: Implement Web of Things Interface ## New Features - **/things Endpoint**: Introduced a new endpoint, `/things`, to retrieve resource links for devices. This endpoint provides a standardized way to access and interact with connected devices. ## Tests - **Endpoint Testing**: Added comprehensive tests for the new `/things` endpoint. These tests ensure that device resources are properly retrieved and formatted, maintaining robustness and reliability in various scenarios. ## Refactor - **JSON Unmarshalling**: Enhanced the JSON unmarshalling process in HTTP gateway tests. This improvement focuses on better error handling and more efficient processing of JSON data, contributing to overall test stability and readability. ## Documentation - **Updated Documentation**: Revised the documentation to reflect changes in the usage of constants and parameters during virtual device creation. The updates include support for conditional resource addition based on the availability of device descriptions, making the documentation more accurate and helpful. ## New Endpoints - **/api/v1/things**: Implemented the `/api/v1/things` endpoint to list all device resources. - **/api/v1/things/{deviceID}**: Implemented the `/api/v1/things/{deviceID}` endpoint to access resources specific to a device identified by `deviceID`. ## Miscellaneous - **Golang Upgrade**: Upgraded the Golang version to 1.22, ensuring compatibility with the latest features and improvements in the Go ecosystem. --------- Co-authored-by: Jozef Kralik --- .github/workflows/builds.yml | 6 +- .github/workflows/checkFormat.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/staticAnalysis.yml | 2 +- .github/workflows/test.yml | 8 + .golangci.yml | 2 +- .vscode/settings.json | 3 +- Dockerfile.test | 2 +- Makefile | 62 ++- bundle/Dockerfile | 2 +- .../observation/deviceObserver_test.go | 7 +- go.mod | 9 +- go.sum | 31 +- http-gateway/Dockerfile | 4 +- http-gateway/service/deleteDevice.go | 2 +- http-gateway/service/getDevice.go | 28 +- .../service/getDeviceResourceLinks.go | 2 +- http-gateway/service/getResource.go | 122 ++++- http-gateway/service/getResource_test.go | 78 +++ http-gateway/service/getThings.go | 392 +++++++++++++++ http-gateway/service/getThings_test.go | 452 ++++++++++++++++++ .../service/getWebConfiguration_test.go | 16 +- http-gateway/service/requestHandler.go | 33 +- http-gateway/service/service.go | 2 +- http-gateway/service/updateResource.go | 12 +- http-gateway/service/updateResource_test.go | 106 +++- http-gateway/swagger.yaml | 9 + http-gateway/test/http.go | 5 + http-gateway/test/test.go | 20 +- http-gateway/uri/uri.go | 9 + test/bridge-device/bridge-device.jsonld | 8 + test/bridge-device/config.yaml | 19 + test/cloud-server/Dockerfile | 2 +- test/device/bridge/device.go | 43 +- test/device/device.go | 25 +- test/device/ocf/device.go | 4 + test/test.go | 54 ++- test/thingDescription.go | 33 ++ test/virtual-device/virtualDevice.go | 22 +- tools/cert-tool/Dockerfile | 2 +- tools/docker/Dockerfile.in | 2 +- 41 files changed, 1528 insertions(+), 116 deletions(-) create mode 100644 http-gateway/service/getThings.go create mode 100644 http-gateway/service/getThings_test.go create mode 100644 test/bridge-device/bridge-device.jsonld create mode 100644 test/bridge-device/config.yaml create mode 100644 test/thingDescription.go diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 844e49223..da7cc5758 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -12,8 +12,8 @@ jobs: matrix: include: # check support for oldest supported golang version - - name: go1.20 - go-version: "~1.20" + - name: go1.22 + go-version: "~1.22" runs-on: ubuntu-latest steps: - name: Checkout @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version || '^1.20' }} + go-version: ${{ matrix.go-version || '^1.22' }} check-latest: true - run: | diff --git a/.github/workflows/checkFormat.yml b/.github/workflows/checkFormat.yml index d81d5976e..a6226037e 100644 --- a/.github/workflows/checkFormat.yml +++ b/.github/workflows/checkFormat.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v5 with: - go-version: "^1.20" # The Go version to download (if necessary) and use. + go-version: "^1.22" # The Go version to download (if necessary) and use. check-latest: true - name: Check formatting diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 54abad392..6a7b33e10 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "^1.20" # The Go version to download (if necessary) and use. + go-version: "^1.22" # The Go version to download (if necessary) and use. check-latest: true cache: false diff --git a/.github/workflows/staticAnalysis.yml b/.github/workflows/staticAnalysis.yml index e32c75abf..8acc7d31f 100644 --- a/.github/workflows/staticAnalysis.yml +++ b/.github/workflows/staticAnalysis.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "^1.20" # The Go version to download (if necessary) and use. + go-version: "^1.22" # The Go version to download (if necessary) and use. check-latest: true - run: go version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 418962e3c..5c7139d79 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,6 +98,14 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + # login to ghcr.io so we can download device/bridge-device package + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run a test run: | make ${{ matrix.cmd }} TEST_CHECK_RACE=${{ matrix.checkRace }} \ diff --git a/.golangci.yml b/.golangci.yml index 5a273e2d1..9a83c0b0a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -173,4 +173,4 @@ issues: # fix: true run: - go: "1.20" + go: "1.22" diff --git a/.vscode/settings.json b/.vscode/settings.json index b9dead665..cfa605fd3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,7 +35,8 @@ "TEST_IDENTITY_STORE_LOG_LEVEL": "info", "TEST_IDENTITY_STORE_LOG_DUMP_BODY": "false", "TEST_DATABASE": "mongoDB", - // "TEST_DEVICE_NAME": "bridged-device", + "TEST_BRIDGE_DEVICE_CONFIG": "${workspaceFolder}/.tmp/bridge/config-test.yaml", + // "TEST_DEVICE_NAME": "bridged-device-0", // "TEST_DEVICE_TYPE": "bridged", // "GODEBUG": "scavtrace=1", // "TEST_COAP_GATEWAY_UDP_ENABLED": "true", diff --git a/Dockerfile.test b/Dockerfile.test index 6de8e49e3..620c3a696 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -6,7 +6,7 @@ RUN apt-get update \ # apt: ca-certificates git make sudo RUN git clone https://github.com/udhos/update-golang.git \ && cd update-golang \ - && sudo RELEASE=1.20.14 ./update-golang.sh \ + && sudo RELEASE=1.22.3 ./update-golang.sh \ && ln -s /usr/local/go/bin/go /usr/bin/go WORKDIR $GOPATH/src/github.com/plgd-dev/hub COPY go.mod go.sum ./ diff --git a/Makefile b/Makefile index 0393820b2..c193868d7 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ TEST_DATABASE ?= mongodb CERT_TOOL_SIGN_ALG ?= ECDSA-SHA256 # supported values: P256, P384, P521 CERT_TOOL_ELLIPTIC_CURVE ?= P256 -CERT_TOOL_IMAGE=ghcr.io/plgd-dev/hub/cert-tool:vnext +CERT_TOOL_IMAGE = ghcr.io/plgd-dev/hub/cert-tool:vnext SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store test/oauth-server tools/cert-tool .PHONY: $(SUBDIRS) push proto/generate clean build test env mongo nats certificates hub-build http-gateway-www simulators @@ -225,6 +225,64 @@ simulators: simulators/clean $(call RUN-DOCKER-DEVICE,$(DEVICE_SIMULATOR_RES_OBSERVABLE_NAME),$(DEVICE_SIMULATOR_RES_OBSERVABLE_IMG)) .PHONY: simulators +BRIDGE_DEVICE_SRC_DIR = $(WORKING_DIRECTORY)/test/bridge-device +BRIDGE_DEVICE_IMAGE = ghcr.io/plgd-dev/device/bridge-device:vnext +BRIDGE_DEVICE_NAME = bridgedev +BRIDGE_DEVICE_ID ?= 8f596b43-29c0-4147-8b40-e99268ab30f7 +BRIDGE_DEVICE_RESOURCES_PER_DEVICE ?= 3 +BRIDGE_DEVICES_COUNT ?= 3 + +define SET-BRIDGE-DEVICE-CONFIG + yq -i '.apis.coap.id = "$(BRIDGE_DEVICE_ID)"' $(1) + yq -i '.apis.coap.externalAddresses=["127.0.0.1:15683","[::1]:15683"]' $(1) + yq -i '.cloud.enabled=true' $(1) + yq -i '.cloud.cloudID="$(CLOUD_SID)"' $(1) + yq -i '.cloud.tls.caPoolPath="$(2)/certs/root_ca.crt"' $(1) + yq -i '.cloud.tls.keyPath="$(2)/certs/coap.key"' $(1) + yq -i '.cloud.tls.certPath="$(2)/certs/coap.crt"' $(1) + yq -i '.numGeneratedBridgedDevices=$(BRIDGE_DEVICES_COUNT)' $(1) + yq -i '.numResourcesPerDevice=$(BRIDGE_DEVICE_RESOURCES_PER_DEVICE)' $(1) + yq -i '.thingDescription.enabled=true' $(1) + yq -i '.thingDescription.file="$(2)/bridge/bridge-device.jsonld"' $(1) +endef + +# config-docker.yaml -> copy of configuration with paths valid inside docker container +# config-test.yaml -> copy of configuration with paths valid on host machine +simulators/bridge/env: simulators/bridge/clean certificates + mkdir -p $(WORKING_DIRECTORY)/.tmp/bridge + cp $(BRIDGE_DEVICE_SRC_DIR)/bridge-device.jsonld $(WORKING_DIRECTORY)/.tmp/bridge/ + cp $(BRIDGE_DEVICE_SRC_DIR)/config.yaml $(WORKING_DIRECTORY)/.tmp/bridge/config-docker.yaml + $(call SET-BRIDGE-DEVICE-CONFIG,$(WORKING_DIRECTORY)/.tmp/bridge/config-docker.yaml,) + cp $(BRIDGE_DEVICE_SRC_DIR)/config.yaml $(WORKING_DIRECTORY)/.tmp/bridge/config-test.yaml + $(call SET-BRIDGE-DEVICE-CONFIG,$(WORKING_DIRECTORY)/.tmp/bridge/config-test.yaml,$(WORKING_DIRECTORY)/.tmp) + +.PHONY: simulators/bridge/env + +define RUN-BRIDGE-DOCKER-DEVICE + docker pull $(BRIDGE_DEVICE_IMAGE) ; \ + docker run \ + -d \ + --name=$(BRIDGE_DEVICE_NAME) \ + --network=host \ + -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ + -v $(WORKING_DIRECTORY)/.tmp/bridge:/bridge \ + $(BRIDGE_DEVICE_IMAGE) -config /bridge/config-docker.yaml +endef + +simulators/bridge: simulators/bridge/env + $(call RUN-BRIDGE-DOCKER-DEVICE) + +.PHONY: simulators/bridge + +simulators/bridge/clean: + rm -rf $(WORKING_DIRECTORY)/.tmp/bridge || : + $(call REMOVE-DOCKER-DEVICE,$(BRIDGE_DEVICE_NAME)) + +.PHONY: simulators/bridge/clean + +simulators: simulators/bridge +simulators/clean: simulators/bridge/clean + env/test/mem: clean certificates nats mongo privateKeys scylla .PHONY: env/test/mem @@ -235,6 +293,7 @@ define RUN-DOCKER docker run \ --rm \ --network=host \ + -v $(WORKING_DIRECTORY)/.tmp/bridge:/bridge \ -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ -v $(WORKING_DIRECTORY)/.tmp/coverage:/coverage \ -v $(WORKING_DIRECTORY)/.tmp/report:/report \ @@ -253,6 +312,7 @@ define RUN-DOCKER -e TEST_OAUTH_SERVER_ID_TOKEN_PRIVATE_KEY=/privKeys/idTokenKey.pem \ -e TEST_OAUTH_SERVER_ACCESS_TOKEN_PRIVATE_KEY=/privKeys/accessTokenKey.pem \ -e TEST_HTTP_GW_WWW_ROOT=/usr/local/www \ + -e TEST_BRIDGE_DEVICE_CONFIG=/bridge/config-docker.yaml \ hub-test \ $(1) ; endef diff --git a/bundle/Dockerfile b/bundle/Dockerfile index 8ca905053..a31a14308 100644 --- a/bundle/Dockerfile +++ b/bundle/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.20.14-alpine AS build +FROM golang:1.22.3-alpine AS build RUN apk add --no-cache curl git build-base WORKDIR $GOPATH/src/github.com/plgd-dev/hub COPY go.mod go.sum ./ diff --git a/coap-gateway/service/observation/deviceObserver_test.go b/coap-gateway/service/observation/deviceObserver_test.go index 19561a498..04e3e489a 100644 --- a/coap-gateway/service/observation/deviceObserver_test.go +++ b/coap-gateway/service/observation/deviceObserver_test.go @@ -34,6 +34,7 @@ import ( coapgwTestService "github.com/plgd-dev/hub/v2/test/coap-gateway/service" coapgwTest "github.com/plgd-dev/hub/v2/test/coap-gateway/test" "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device/ocf" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -299,13 +300,13 @@ func testPreregisterVirtualDevice(ctx context.Context, t *testing.T, deviceID st require.NoError(t, err) require.NotEmpty(t, ev.GetOperationProcessed()) require.Equal(t, pb.Event_OperationProcessed_ErrorStatus_OK, ev.GetOperationProcessed().GetErrorStatus().GetCode()) - virtualdevice.CreateDevice(ctx, t, "name-"+deviceID, deviceID, numResources, test.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), isClient, raClient) - resources := virtualdevice.CreateDeviceResourceLinks(deviceID, numResources) + virtualdevice.CreateDevice(ctx, t, "name-"+deviceID, deviceID, numResources, false, test.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), isClient, raClient) + resources := virtualdevice.CreateDeviceResourceLinks(deviceID, numResources, false) links := make([]schema.ResourceLink, 0, len(resources)) for _, r := range resources { links = append(links, r.ToSchema()) } - test.WaitForDevice(t, client, deviceID, ev.GetSubscriptionId(), ev.GetCorrelationId(), links) + test.WaitForDevice(t, client, ocf.NewDevice(deviceID, test.TestDeviceName), ev.GetSubscriptionId(), ev.GetCorrelationId(), links) } func testValidateResourceLinks(ctx context.Context, t *testing.T, deviceID string, grpcClient pb.GrpcGatewayClient, _ raPb.ResourceAggregateClient) { diff --git a/go.mod b/go.mod index 41c5cdb1d..7de771a99 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/plgd-dev/hub/v2 -go 1.20 +go 1.22 + +toolchain go1.22.0 require ( github.com/favadi/protoc-go-inject-tag v1.4.0 @@ -28,7 +30,7 @@ require ( github.com/panjf2000/ants/v2 v2.9.1 github.com/pion/dtls/v2 v2.2.8-0.20240501061905-2c36d63320a0 github.com/pion/logging v0.2.2 - github.com/plgd-dev/device/v2 v2.5.1-0.20240502072920-6021006ea86b + github.com/plgd-dev/device/v2 v2.5.1-0.20240513064831-b553d1a87e1c github.com/plgd-dev/go-coap/v3 v3.3.4 github.com/plgd-dev/kit/v2 v2.0.0-20211006190727-057b33161b90 github.com/pseudomuto/protoc-gen-doc v1.5.1 @@ -38,6 +40,7 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/ugorji/go/codec v1.2.12 github.com/vincent-petithory/dataurl v1.0.0 + github.com/web-of-things-open-source/thingdescription-go v0.0.0-20240510130416-741fef736e1e go.mongodb.org/mongo-driver v1.15.0 go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.49.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 @@ -71,6 +74,8 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dsnet/golib/memfile v1.0.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/fredbi/uri v1.1.0 // indirect + github.com/go-json-experiment/json v0.0.0-20240418180308-af2d5061e6c2 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect diff --git a/go.sum b/go.sum index 76fbaf440..588ba6832 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -44,8 +45,12 @@ github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/favadi/protoc-go-inject-tag v1.4.0 h1:K3KXxbgRw5WT4f43LbglARGz/8jVsDOS7uMjG4oNvXY= github.com/favadi/protoc-go-inject-tag v1.4.0/go.mod h1:AZ+PK+QDKUOLlBRG0rYiKkUX5Hw7+7GTFzlU99GFSbQ= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= +github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= @@ -57,6 +62,8 @@ github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXE github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= github.com/go-co-op/gocron/v2 v2.3.0 h1:UmXdUuql3h/+JN3rFDhZdDvgeCjR+r/zrSsQNZje8uo= github.com/go-co-op/gocron/v2 v2.3.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw= +github.com/go-json-experiment/json v0.0.0-20240418180308-af2d5061e6c2 h1:lhCu2IkNoFfDdcjHos2ZtLdAsyxLZbkpijNzhvvM6BY= +github.com/go-json-experiment/json v0.0.0-20240418180308-af2d5061e6c2/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -106,9 +113,12 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -165,9 +175,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -212,8 +224,18 @@ github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkL github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/plgd-dev/device/v2 v2.5.1-0.20240502072920-6021006ea86b h1:AF0/TUHEdt/fqQT0GjEoIXe53XB8qsOHzOu6Npg19Ig= -github.com/plgd-dev/device/v2 v2.5.1-0.20240502072920-6021006ea86b/go.mod h1:y/OnxPhE+FLRcme8i0/bEpeP8qWO8+KFRpU6nTgsozo= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510131130-e10f602b7f77 h1:bxo0wjO1xpTvEYMgWex2rdQLgSTw1yWbaWJg37R6tXc= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510131130-e10f602b7f77/go.mod h1:2mFPs55x2Li76zkrHdRNY3yOqVWSh59hiUw+6FYXA0k= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510134226-6cd2643faa0a h1:HXUPcCLl6eHqp9zIxFRgwC9GgmBJUb/CU53Ql0S//Js= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510134226-6cd2643faa0a/go.mod h1:2mFPs55x2Li76zkrHdRNY3yOqVWSh59hiUw+6FYXA0k= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510135039-6a9dce16a657 h1:/RhBj4mSkB3+KN5ra5xT84fwQhfSZZTOpKRYWw+zueY= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510135039-6a9dce16a657/go.mod h1:2mFPs55x2Li76zkrHdRNY3yOqVWSh59hiUw+6FYXA0k= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510135326-9604b2607b7f h1:G0kV13UqowsZoyIQ5y3q8ZlgzG/euaCf+p+9gryZtw4= +github.com/plgd-dev/device/v2 v2.5.1-0.20240510135326-9604b2607b7f/go.mod h1:2mFPs55x2Li76zkrHdRNY3yOqVWSh59hiUw+6FYXA0k= +github.com/plgd-dev/device/v2 v2.5.1-0.20240513064831-b553d1a87e1c h1:kNF2KvyCzA8IMERdHUrL/LMdsuZM/tXGjLVzEX2lcg4= +github.com/plgd-dev/device/v2 v2.5.1-0.20240513064831-b553d1a87e1c/go.mod h1:2mFPs55x2Li76zkrHdRNY3yOqVWSh59hiUw+6FYXA0k= github.com/plgd-dev/go-coap/v2 v2.0.4-0.20200819112225-8eb712b901bc/go.mod h1:+tCi9Q78H/orWRtpVWyBgrr4vKFo2zYtbbxUllerBp4= github.com/plgd-dev/go-coap/v2 v2.4.1-0.20210517130748-95c37ac8e1fa/go.mod h1:rA7fc7ar+B/qa+Q0hRqv7yj/EMtIlmo1l7vkQGSrHPU= github.com/plgd-dev/go-coap/v3 v3.3.4 h1:clDLFOXXmXfhZqB0eSk6WJs2iYfjC2J22Ixwu5MHiO0= @@ -232,6 +254,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -273,6 +296,8 @@ github.com/valyala/fasthttp v1.12.0/go.mod h1:229t1eWu9UXTPmoUkbpN/fctKPBY4IJoFX github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +github.com/web-of-things-open-source/thingdescription-go v0.0.0-20240510130416-741fef736e1e h1:blQyU8WqqyRcBmaAPLiU5cTg9BSQu04CJZ/ffEzgI1s= +github.com/web-of-things-open-source/thingdescription-go v0.0.0-20240510130416-741fef736e1e/go.mod h1:L/jWuWf+v7rmuFykpUP/runRXTnnA0QdGGgou8vzPrw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -315,6 +340,7 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -488,6 +514,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= diff --git a/http-gateway/Dockerfile b/http-gateway/Dockerfile index 452259cf4..ea2ea05a4 100644 --- a/http-gateway/Dockerfile +++ b/http-gateway/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.20.14-alpine AS build +FROM golang:1.22.3-alpine AS build ARG VERSION ARG COMMIT_DATE ARG SHORT_COMMIT @@ -16,7 +16,7 @@ RUN ( patch -p1 < "$GOPATH/src/github.com/plgd-dev/hub/tools/docker/patches/shri WORKDIR $GOPATH/src/github.com/plgd-dev/hub/vendor/golang.org/x/oauth2 RUN ( patch -p1 < "$GOPATH/src/github.com/plgd-dev/hub/tools/docker/patches/golang_org_x_oauth2_propagate_error.patch" ) WORKDIR $GOPATH/src/github.com/plgd-dev/hub/http-gateway -RUN CGO_ENABLED=0 go build \ +RUN go build \ -mod=vendor \ -ldflags "-linkmode external -extldflags -static -X github.com/plgd-dev/hub/v2/pkg/build.CommitDate=$COMMIT_DATE -X github.com/plgd-dev/hub/v2/pkg/build.CommitHash=$SHORT_COMMIT -X github.com/plgd-dev/hub/v2/pkg/build.BuildDate=$DATE -X github.com/plgd-dev/hub/v2/pkg/build.Version=$VERSION -X github.com/plgd-dev/hub/v2/pkg/build.ReleaseURL=$RELEASE_URL" \ -o /go/bin/http-gateway \ diff --git a/http-gateway/service/deleteDevice.go b/http-gateway/service/deleteDevice.go index 61748966e..a43eb8076 100644 --- a/http-gateway/service/deleteDevice.go +++ b/http-gateway/service/deleteDevice.go @@ -14,7 +14,7 @@ func (requestHandler *RequestHandler) deleteDevice(w http.ResponseWriter, r *htt serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot delete device('%v'): %v", deviceID, err)) return } - toSimpleResponse(w, rec, func(w http.ResponseWriter, err error) { + toSimpleResponse(w, rec, false, func(w http.ResponseWriter, err error) { serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot delete device('%v'): %v", deviceID, err)) }, streamResponseKey) } diff --git a/http-gateway/service/getDevice.go b/http-gateway/service/getDevice.go index d9119ed00..8a3eac3c7 100644 --- a/http-gateway/service/getDevice.go +++ b/http-gateway/service/getDevice.go @@ -65,7 +65,10 @@ func writeSimpleResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, } } -func toSimpleResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, writeError func(w http.ResponseWriter, err error), responseKeys ...string) { +func getResponse(rec *httptest.ResponseRecorder, allowEmpty bool, responseKeys ...string) (interface{}, error) { + if len(rec.Body.Bytes()) == 0 && allowEmpty { + return nil, nil + } iter := json.NewDecoder(bytes.NewReader(rec.Body.Bytes())) datas := make([]interface{}, 0, 1) for { @@ -75,21 +78,17 @@ func toSimpleResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, wri break } if err != nil { - writeError(w, err) - return + return nil, err } datas = append(datas, v) } if len(datas) == 0 { - writeError(w, kitNetGrpc.ForwardErrorf(codes.NotFound, "not found")) - return + return nil, kitNetGrpc.ForwardErrorf(codes.NotFound, "not found") } if len(datas) != 1 { - writeError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "invalid number of responses")) - return + return nil, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "invalid number of responses") } - var result interface{} - result = datas[0] + result := datas[0] for _, key := range responseKeys { m, ok := result.(map[string]interface{}) if !ok { @@ -102,6 +101,15 @@ func toSimpleResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, wri break } } + return result, nil +} + +func toSimpleResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, allowEmpty bool, writeError func(w http.ResponseWriter, err error), responseKeys ...string) { + result, err := getResponse(rec, allowEmpty, responseKeys...) + if err != nil { + writeError(w, err) + return + } writeSimpleResponse(w, rec, result, writeError) } @@ -131,7 +139,7 @@ func (requestHandler *RequestHandler) getDevice(w http.ResponseWriter, r *http.R serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot get device('%v'): %v", deviceID, err)) return } - toSimpleResponse(w, rec, func(w http.ResponseWriter, err error) { + toSimpleResponse(w, rec, false, func(w http.ResponseWriter, err error) { serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot get device('%v'): %v", deviceID, err)) }, streamResponseKey) } diff --git a/http-gateway/service/getDeviceResourceLinks.go b/http-gateway/service/getDeviceResourceLinks.go index bc9c865b8..7eb6b3d90 100644 --- a/http-gateway/service/getDeviceResourceLinks.go +++ b/http-gateway/service/getDeviceResourceLinks.go @@ -40,7 +40,7 @@ func (requestHandler *RequestHandler) getDeviceResourceLinks(w http.ResponseWrit rec := httptest.NewRecorder() requestHandler.mux.ServeHTTP(rec, r) - toSimpleResponse(w, rec, func(w http.ResponseWriter, err error) { + toSimpleResponse(w, rec, false, func(w http.ResponseWriter, err error) { serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot get device('%v') resource links: %v", deviceID, err)) }, streamResponseKey) } diff --git a/http-gateway/service/getResource.go b/http-gateway/service/getResource.go index 1f0272cd6..e3e0eb2a4 100644 --- a/http-gateway/service/getResource.go +++ b/http-gateway/service/getResource.go @@ -2,18 +2,22 @@ package service import ( "encoding/base64" + "fmt" "net/http" "net/http/httptest" + "strconv" "strings" "github.com/google/go-querystring/query" "github.com/gorilla/mux" + "github.com/plgd-dev/device/v2/pkg/codec/json" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/http-gateway/serverMux" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/protojson" ) const errFmtFromTwin = "cannot get resource('%v') from twin: %w" @@ -47,7 +51,7 @@ func getETags(r *http.Request) [][]byte { return etags } -func (requestHandler *RequestHandler) getResourceFromTwin(w http.ResponseWriter, r *http.Request, resourceID *pb.ResourceIdFilter) { +func (requestHandler *RequestHandler) getResourceFromTwin(r *http.Request, resourceID *pb.ResourceIdFilter) (*httptest.ResponseRecorder, error) { type Options struct { ResourceIDFilter []string `url:"httpResourceIdFilter"` } @@ -57,33 +61,35 @@ func (requestHandler *RequestHandler) getResourceFromTwin(w http.ResponseWriter, v, err := query.Values(opt) if err != nil { - serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, errFmtFromTwin, resourceID, err)) - return + return nil, err } r.URL.Path = uri.Resources r.URL.RawQuery = v.Encode() rec := httptest.NewRecorder() requestHandler.mux.ServeHTTP(rec, r) + return rec, nil +} - toSimpleResponse(w, rec, func(w http.ResponseWriter, err error) { - serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, errFmtFromTwin, resourceID, err)) - }, streamResponseKey) +func parseBoolQuery(str string) bool { + val, err := strconv.ParseBool(str) + if err != nil { + return false + } + return val } -func (requestHandler *RequestHandler) getResource(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - deviceID := vars[uri.DeviceIDKey] - resourceHref := vars[uri.ResourceHrefKey] - twin := r.URL.Query().Get(uri.TwinQueryKey) - resourceInterface := r.URL.Query().Get(uri.ResourceInterfaceQueryKey) +func (requestHandler *RequestHandler) serveResourceRequest(r *http.Request, deviceID, resourceHref, twin, resourceInterface string) (*httptest.ResponseRecorder, error) { resourceID := pb.ResourceIdFilter{ ResourceId: commands.NewResourceID(deviceID, resourceHref), Etag: getETags(r), } - if (twin == "" || strings.ToLower(twin) == "true") && resourceInterface == "" { - requestHandler.getResourceFromTwin(w, r, &resourceID) - return + if (twin == "" || parseBoolQuery(twin)) && resourceInterface == "" { + rec, err := requestHandler.getResourceFromTwin(r, &resourceID) + if err != nil { + return nil, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, errFmtFromTwin, &resourceID, err) + } + return rec, nil } query := r.URL.Query() @@ -97,7 +103,87 @@ func (requestHandler *RequestHandler) getResource(w http.ResponseWriter, r *http rec := httptest.NewRecorder() requestHandler.mux.ServeHTTP(rec, r) - toSimpleResponse(w, rec, func(w http.ResponseWriter, err error) { - serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot get resource('%v') from the device: %v", resourceID.ToString(), err)) - }) + return rec, nil +} + +func jsonGetValueOnPath(v interface{}, path ...string) (interface{}, error) { + for idx, p := range path { + if v == nil { + return nil, fmt.Errorf("doesn't contains %v", strings.Join(path[:idx+1], ".")) + } + m, ok := v.(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("%v is not a map but %T", strings.Join(path[:idx+1], "."), v) + } + v, ok = m[p] + if !ok { + return nil, fmt.Errorf("doesn't contains %v", strings.Join(path[:idx+1], ".")) + } + } + return v, nil +} + +func isContentEmpty(data []byte) bool { + if len(data) == 0 { + return true + } + var ct commands.Content + err := protojson.Unmarshal(data, &ct) + if err != nil { + return false + } + return len(ct.GetData()) == 0 && ct.GetCoapContentFormat() == -1 +} + +func (requestHandler *RequestHandler) filterOnlyContent(rec *httptest.ResponseRecorder, contentPath ...string) (resetContent bool) { + if rec.Code == http.StatusNotModified { + rec.Body.Reset() + return false + } + if rec.Code != http.StatusOK { + return false + } + var v map[interface{}]interface{} + err := json.Decode(rec.Body.Bytes(), &v) + if err != nil { + requestHandler.logger.Debugf("filter only content: cannot decode response : %v", err) + return false + } + content, err := jsonGetValueOnPath(v, contentPath...) + if err != nil { + requestHandler.logger.With("body", v).Debugf("filter only content: %v", err) + return false + } + body, err := json.Encode(content) + if err != nil { + requestHandler.logger.With("body", v).Debugf("filter only content: cannot encode 'content' object: %v", err) + return false + } + rec.Body.Reset() + if !isContentEmpty(body) { + _, _ = rec.Body.Write(body) + return false + } + return true +} + +func (requestHandler *RequestHandler) getResource(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + deviceID := vars[uri.DeviceIDKey] + resourceHref := vars[uri.ResourceHrefKey] + twin := r.URL.Query().Get(uri.TwinQueryKey) + onlyContent := r.URL.Query().Get(uri.OnlyContentQueryKey) + resourceInterface := r.URL.Query().Get(uri.ResourceInterfaceQueryKey) + rec, err := requestHandler.serveResourceRequest(r, deviceID, resourceHref, twin, resourceInterface) + if err != nil { + serverMux.WriteError(w, err) + return + } + allowEmptyContent := false + if parseBoolQuery(onlyContent) { + allowEmptyContent = requestHandler.filterOnlyContent(rec, "result", "data", "content") + } + toSimpleResponse(w, rec, allowEmptyContent, func(w http.ResponseWriter, err error) { + serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot get resource('%v/%v') from the device: %v", deviceID, resourceHref, err)) + }, streamResponseKey) } diff --git a/http-gateway/service/getResource_test.go b/http-gateway/service/getResource_test.go index f71eb6400..3cd1b3f65 100644 --- a/http-gateway/service/getResource_test.go +++ b/http-gateway/service/getResource_test.go @@ -9,6 +9,7 @@ import ( "net/http" "testing" + "github.com/plgd-dev/device/v2/pkg/codec/json" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/test/resource/types" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" @@ -215,3 +216,80 @@ func TestRequestHandlerGetResource(t *testing.T) { }) } } + +func TestRequestHandlerGetResourceWithOnlyContent(t *testing.T) { + deviceID := test.MustFindDeviceByName(test.TestDeviceName) + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := service.SetUp(ctx, t) + defer tearDown() + + shutdownHttp := httpgwTest.SetUp(t) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + _, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + type args struct { + deviceID string + resourceHref string + } + tests := []struct { + name string + args args + want interface{} + wantCode int + }{ + { + name: "json: get from resource twin", + args: args{ + deviceID: deviceID, + resourceHref: test.TestResourceLightInstanceHref("1"), + }, + want: map[interface{}]interface{}{"name": "Light", "power": uint64(0x0), "state": false}, + wantCode: http.StatusOK, + }, + { + name: "json: not exists", + args: args{ + deviceID: deviceID, + resourceHref: "/not-exists", + }, + wantCode: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpgwTest.NewRequest(http.MethodGet, uri.AliasDeviceResource, nil).AuthToken(token) + rb.DeviceId(tt.args.deviceID).ResourceHref(tt.args.resourceHref) + rb.OnlyContent(true) + resp := httpgwTest.HTTPDo(t, rb.Build()) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantCode, resp.StatusCode) + if tt.wantCode != http.StatusOK { + return + } + var got interface{} + err := json.ReadFrom(resp.Body, &got) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/http-gateway/service/getThings.go b/http-gateway/service/getThings.go new file mode 100644 index 000000000..468248eac --- /dev/null +++ b/http-gateway/service/getThings.go @@ -0,0 +1,392 @@ +package service + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + + "github.com/google/uuid" + "github.com/gorilla/mux" + jsoniter "github.com/json-iterator/go" + bridgeDeviceTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + "github.com/plgd-dev/device/v2/bridge/resources" + bridgeResourcesTD "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" + schemaCloud "github.com/plgd-dev/device/v2/schema/cloud" + schemaCredential "github.com/plgd-dev/device/v2/schema/credential" + schemaDevice "github.com/plgd-dev/device/v2/schema/device" + schemaMaintenance "github.com/plgd-dev/device/v2/schema/maintenance" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/http-gateway/uri" + "github.com/plgd-dev/hub/v2/pkg/log" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/pkg/security/openid" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" + wotTD "github.com/web-of-things-open-source/thingdescription-go/thingDescription" + "google.golang.org/grpc/codes" +) + +type ThingLink struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +type GetThingsResponse struct { + Base string `json:"base"` + Security *wotTD.TypeDeclaration `json:"security"` + ID string `json:"id"` + SecurityDefinitions map[string]wotTD.SecurityScheme `json:"securityDefinitions"` + Links []ThingLink `json:"links"` +} + +const ( + ThingLinkRelationItem = "item" + ThingLinkRelationCollection = "collection" +) + +func (requestHandler *RequestHandler) getResourceLinks(ctx context.Context, deviceFilter []string, typeFilter []string) ([]*events.ResourceLinksPublished, error) { + client, err := requestHandler.client.GrpcGatewayClient().GetResourceLinks(ctx, &pb.GetResourceLinksRequest{ + DeviceIdFilter: deviceFilter, + TypeFilter: typeFilter, + }) + if err != nil { + return nil, fmt.Errorf("cannot get resource links: %w", err) + } + links := make([]*events.ResourceLinksPublished, 0, 16) + for { + link, err := client.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("cannot receive resource link: %w", err) + } + links = append(links, link) + } + return links, nil +} + +func (requestHandler *RequestHandler) getThings(w http.ResponseWriter, r *http.Request) { + resLinks, err := requestHandler.getResourceLinks(r.Context(), nil, []string{bridgeResourcesTD.ResourceType}) + if err != nil { + serverMux.WriteError(w, err) + return + } + hubCfg, err := requestHandler.client.GrpcGatewayClient().GetHubConfiguration(r.Context(), &pb.HubConfigurationRequest{}) + if err != nil { + serverMux.WriteError(w, err) + return + } + + links := make([]ThingLink, 0, len(resLinks)) + for _, l := range resLinks { + links = append(links, ThingLink{ + Href: "/" + l.GetDeviceId(), + Rel: ThingLinkRelationItem, + }) + } + var td wotTD.ThingDescription + ThingSetSecurity(&td, requestHandler.openIDConfig) + + things := GetThingsResponse{ + Base: requestHandler.config.UI.WebConfiguration.HTTPGatewayAddress + uri.Things, + Links: links, + Security: td.Security, + SecurityDefinitions: td.SecurityDefinitions, + ID: "urn:uuid:" + hubCfg.GetId(), + } + if err := jsonResponseWriter(w, things); err != nil { + log.Errorf("failed to write response: %v", err) + } +} + +func CreateHTTPForms(hrefUri *url.URL, opsBits resources.SupportedOperation, contentType message.MediaType) []wotTD.FormElementProperty { + supportedByOps := map[resources.SupportedOperation]wotTD.StickyDescription{ + resources.SupportedOperationRead: wotTD.Readproperty, + resources.SupportedOperationWrite: wotTD.Writeproperty, + } + + ops := make([]string, 0, len(supportedByOps)) + for opBit, op := range supportedByOps { + if opsBits.HasOperation(opBit) { + ops = append(ops, string(op)) + } + } + if len(ops) == 0 { + return nil + } + q := hrefUri.Query() + if len(q) > 0 && q.Has("di") { + q.Del("di") + } + q.Add(uri.OnlyContentQueryKey, "1") + hrefUri.RawQuery = q.Encode() + return []wotTD.FormElementProperty{ + { + ContentType: bridgeDeviceTD.StringToPtr(contentType.String()), + Href: *hrefUri, + Op: &wotTD.FormElementPropertyOp{ + StringArray: ops, + }, + }, + } +} + +func patchProperty(pe wotTD.PropertyElement, deviceID, href string, contentType message.MediaType) (wotTD.PropertyElement, error) { + deviceUUID, err := uuid.Parse(deviceID) + if err != nil { + return wotTD.PropertyElement{}, fmt.Errorf("cannot parse deviceID: %w", err) + } + propertyBaseURL := "/" + uri.DevicesPathKey + "/" + deviceID + "/" + uri.ResourcesPathKey + patchFnMap := map[string]func(wotTD.PropertyElement) (wotTD.PropertyElement, error){ + schemaDevice.ResourceURI: func(pe wotTD.PropertyElement) (wotTD.PropertyElement, error) { + return bridgeResourcesTD.PatchDeviceResourcePropertyElement(pe, deviceUUID, propertyBaseURL, contentType, "", CreateHTTPForms) + }, + schemaMaintenance.ResourceURI: func(pe wotTD.PropertyElement) (wotTD.PropertyElement, error) { + return bridgeResourcesTD.PatchMaintenanceResourcePropertyElement(pe, deviceUUID, propertyBaseURL, contentType, CreateHTTPForms) + }, + schemaCloud.ResourceURI: func(pe wotTD.PropertyElement) (wotTD.PropertyElement, error) { + return bridgeResourcesTD.PatchCloudResourcePropertyElement(pe, deviceUUID, propertyBaseURL, contentType, CreateHTTPForms) + }, + schemaCredential.ResourceURI: func(pe wotTD.PropertyElement) (wotTD.PropertyElement, error) { + return bridgeResourcesTD.PatchCredentialResourcePropertyElement(pe, deviceUUID, propertyBaseURL, contentType, CreateHTTPForms) + }, + } + patchFn, ok := patchFnMap[href] + if ok { + pe, err = patchFn(pe) + if err != nil { + return wotTD.PropertyElement{}, err + } + return pe, nil + } + + propOps := bridgeDeviceTD.GetPropertyElementOperations(pe) + pe, err = bridgeDeviceTD.PatchPropertyElement(pe, nil, deviceUUID, propertyBaseURL+href, + propOps.ToSupportedOperations(), contentType, CreateHTTPForms) + if err != nil { + return wotTD.PropertyElement{}, err + } + return pe, nil +} + +var validRefs = map[string]struct{}{ + ThingLinkRelationItem: {}, + ThingLinkRelationCollection: {}, +} + +func isDeviceLink(le wotTD.IconLinkElement) (string, bool) { + if le.Href == "" { + return "", false + } + if le.Href[0] != '/' { + return "", false + } + if le.Rel == nil { + return "", false + } + + if _, ok := validRefs[*le.Rel]; !ok { + return "", false + } + linkedDeviceID := le.Href + if linkedDeviceID[0] == '/' { + linkedDeviceID = linkedDeviceID[1:] + } + uuidDeviceID, err := uuid.Parse(linkedDeviceID) + if err != nil { + return "", false + } + if uuidDeviceID == uuid.Nil { + return "", false + } + return linkedDeviceID, true +} + +func getLinkedDevices(links []wotTD.IconLinkElement) []string { + devices := make([]string, 0, len(links)) + for _, l := range links { + if deviceID, ok := isDeviceLink(l); ok { + devices = append(devices, deviceID) + } + } + return devices +} + +func ThingPatchLink(le wotTD.IconLinkElement, validateDevice map[string]struct{}) (wotTD.IconLinkElement, bool) { + if le.Href == "" { + return wotTD.IconLinkElement{}, false + } + device, ok := isDeviceLink(le) + if !ok { + return le, true + } + if len(validateDevice) == 0 { + return wotTD.IconLinkElement{}, false + } + if _, ok := validateDevice[device]; !ok { + return wotTD.IconLinkElement{}, false + } + le.Href = "/" + uri.ThingsPathKey + le.Href + return le, true +} + +func makeDevicePropertiesValidator(deviceID string, links []*events.ResourceLinksPublished) (map[string]struct{}, bool) { + for _, l := range links { + if l.GetDeviceId() == deviceID { + validateProperties := map[string]struct{}{} + for _, r := range l.GetResources() { + validateProperties[r.GetHref()] = struct{}{} + } + return validateProperties, true + } + } + return nil, false +} + +func makeDeviceLinkValidator(links []*events.ResourceLinksPublished) map[string]struct{} { + validator := make(map[string]struct{}) + for _, l := range links { + validator[l.GetDeviceId()] = struct{}{} + } + return validator +} + +func (requestHandler *RequestHandler) thingSetBase(td *wotTD.ThingDescription) error { + baseURL := requestHandler.config.UI.WebConfiguration.HTTPGatewayAddress + uri.API + base, err := url.Parse(baseURL) + if err != nil { + return fmt.Errorf("cannot parse base url: %w", err) + } + td.Base = *base + return nil +} + +func (requestHandler *RequestHandler) thingSetProperties(ctx context.Context, deviceID string, td *wotTD.ThingDescription) error { + deviceLinks, err := requestHandler.getResourceLinks(ctx, []string{deviceID}, nil) + if err != nil { + return fmt.Errorf("cannot get resource links: %w", err) + } + validateProperties, ok := makeDevicePropertiesValidator(deviceID, deviceLinks) + if !ok { + return fmt.Errorf("cannot get resource links for device %v", deviceID) + } + for href, prop := range td.Properties { + _, ok := validateProperties[href] + if !ok { + _, ok = validateProperties["/"+href] + } + if !ok { + delete(td.Properties, href) + continue + } + patchedProp, err := patchProperty(prop, deviceID, href, message.AppJSON) + if err != nil { + return fmt.Errorf("cannot patch device resource property element: %w", err) + } + td.Properties[href] = patchedProp + } + return nil +} + +func (requestHandler *RequestHandler) thingSetLinks(ctx context.Context, td *wotTD.ThingDescription) { + linkedDevices := getLinkedDevices(td.Links) + var validLinkedDevices map[string]struct{} + if len(linkedDevices) > 0 { + links, err := requestHandler.getResourceLinks(ctx, linkedDevices, []string{bridgeResourcesTD.ResourceType}) + if err == nil { + validLinkedDevices = makeDeviceLinkValidator(links) + } + } + patchedLinks := make([]wotTD.IconLinkElement, 0, len(td.Links)) + for _, link := range td.Links { + patchedLink, ok := ThingPatchLink(link, validLinkedDevices) + if !ok { + continue + } + patchedLinks = append(patchedLinks, patchedLink) + } + if len(patchedLinks) == 0 { + td.Links = nil + } else { + td.Links = patchedLinks + } +} + +func ThingSetSecurity(td *wotTD.ThingDescription, openIDConfig openid.Config) { + td.Security = &wotTD.TypeDeclaration{ + String: bridgeDeviceTD.StringToPtr("oauth2_sc"), + } + td.SecurityDefinitions = map[string]wotTD.SecurityScheme{ + "oauth2_sc": { + Scheme: "oauth2", + Flow: bridgeDeviceTD.StringToPtr("code"), + Authorization: &openIDConfig.AuthURL, + Token: &openIDConfig.TokenURL, + }, + } +} + +func (requestHandler *RequestHandler) thingDescriptionResponse(ctx context.Context, w http.ResponseWriter, rec *httptest.ResponseRecorder, writeError func(w http.ResponseWriter, err error), deviceID string) { + content := jsoniter.Get(rec.Body.Bytes(), streamResponseKey, "data", "content") + if content.ValueType() != jsoniter.ObjectValue { + writeError(w, errors.New("cannot decode thingDescription content")) + return + } + var td wotTD.ThingDescription + err := td.UnmarshalJSON([]byte(content.ToString())) + if err != nil { + writeError(w, fmt.Errorf("cannot decode thingDescription content: %w", err)) + return + } + + // .security + ThingSetSecurity(&td, requestHandler.openIDConfig) + + // .base + if err = requestHandler.thingSetBase(&td); err != nil { + writeError(w, fmt.Errorf("cannot set base url: %w", err)) + return + } + + // .properties.forms + if err = requestHandler.thingSetProperties(ctx, deviceID, &td); err != nil { + writeError(w, fmt.Errorf("cannot set properties: %w", err)) + } + + // .links + requestHandler.thingSetLinks(ctx, &td) + + // marshal thingDescription + data, err := td.MarshalJSON() + if err != nil { + writeError(w, fmt.Errorf("cannot encode thingDescription: %w", err)) + return + } + // copy everything from response recorder + // to actual response writer + for k, v := range rec.Header() { + w.Header()[k] = v + } + w.WriteHeader(rec.Code) + _, err = w.Write(data) + if err != nil { + writeError(w, kitNetGrpc.ForwardErrorf(codes.Internal, "cannot encode response: %v", err)) + } +} + +func (requestHandler *RequestHandler) getThing(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + deviceID := vars[uri.DeviceIDKey] + rec, err := requestHandler.serveResourceRequest(r, deviceID, bridgeResourcesTD.ResourceURI, "", "") + if err != nil { + serverMux.WriteError(w, err) + return + } + requestHandler.thingDescriptionResponse(r.Context(), w, rec, serverMux.WriteError, deviceID) +} diff --git a/http-gateway/service/getThings_test.go b/http-gateway/service/getThings_test.go new file mode 100644 index 000000000..176c9142d --- /dev/null +++ b/http-gateway/service/getThings_test.go @@ -0,0 +1,452 @@ +package service_test + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "testing" + + "github.com/google/uuid" + bridgeTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + bridgeResourcesTD "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" + "github.com/plgd-dev/device/v2/client/core" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" + "github.com/plgd-dev/device/v2/pkg/codec/json" + deviceCoap "github.com/plgd-dev/device/v2/pkg/net/coap" + schemaDevice "github.com/plgd-dev/device/v2/schema/device" + schemaMaintenance "github.com/plgd-dev/device/v2/schema/maintenance" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + httpgwService "github.com/plgd-dev/hub/v2/http-gateway/service" + httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" + httpgwUri "github.com/plgd-dev/hub/v2/http-gateway/uri" + isPb "github.com/plgd-dev/hub/v2/identity-store/pb" + isTest "github.com/plgd-dev/hub/v2/identity-store/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/pkg/security/openid" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + raPb "github.com/plgd-dev/hub/v2/resource-aggregate/service" + raTest "github.com/plgd-dev/hub/v2/resource-aggregate/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device/bridge" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + vd "github.com/plgd-dev/hub/v2/test/virtual-device" + "github.com/stretchr/testify/require" + wotTD "github.com/web-of-things-open-source/thingdescription-go/thingDescription" + "golang.org/x/sync/semaphore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type virtualDevice struct { + name string + deviceID string + tdEnabled bool +} + +func createDevices(ctx context.Context, t *testing.T, numDevices int, protocol commands.Connection_Protocol) []virtualDevice { + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + isConn, err := grpc.NewClient(config.IDENTITY_STORE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = isConn.Close() + }() + isClient := isPb.NewIdentityStoreClient(isConn) + + raConn, err := grpc.NewClient(config.RESOURCE_AGGREGATE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = raConn.Close() + }() + raClient := raPb.NewResourceAggregateClient(raConn) + + devices := make([]virtualDevice, 0, numDevices) + for i := 0; i < numDevices; i++ { + tdEnabled := (i%2 == 0) + devices = append(devices, virtualDevice{ + name: fmt.Sprintf("dev-%v", i), + deviceID: uuid.NewString(), + tdEnabled: tdEnabled, + }) + } + + numGoRoutines := int64(8) + sem := semaphore.NewWeighted(numGoRoutines) + for i := range devices { + err = sem.Acquire(ctx, 1) + require.NoError(t, err) + go func(dev virtualDevice) { + vd.CreateDevice(ctx, t, dev.name, dev.deviceID, 0, dev.tdEnabled, protocol, isClient, raClient) + sem.Release(1) + }(devices[i]) + } + err = sem.Acquire(ctx, numGoRoutines) + require.NoError(t, err) + return devices +} + +func TestRequestHandlerGetThings(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate + isConfig := isTest.MakeConfig(t) + isConfig.APIs.GRPC.TLS.ClientCertificateRequired = false + raConfig := raTest.MakeConfig(t) + raConfig.APIs.GRPC.TLS.ClientCertificateRequired = false + tearDown := service.SetUpServices(ctx, t, services, service.WithISConfig(isConfig), service.WithRAConfig(raConfig)) + defer tearDown() + + httpgwCfg := httpgwTest.MakeConfig(t, true) + shutdownHttp := httpgwTest.New(t, httpgwCfg) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + numDevices := 10 + vds := createDevices(ctx, t, numDevices, test.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME)) + + rb := httpgwTest.NewRequest(http.MethodGet, httpgwUri.Things, nil).AuthToken(token) + resp := httpgwTest.HTTPDo(t, rb.Build()) + defer func() { + _ = resp.Body.Close() + }() + + var v httpgwService.GetThingsResponse + err := httpgwTest.UnmarshalJson(resp.StatusCode, resp.Body, &v) + require.NoError(t, err) + require.Equal(t, httpgwCfg.UI.WebConfiguration.HTTPGatewayAddress+httpgwUri.Things, v.Base) + vdsWithTD := []virtualDevice{} + for _, vd := range vds { + if vd.tdEnabled { + vdsWithTD = append(vdsWithTD, vd) + } + } + require.Len(t, v.Links, len(vdsWithTD)) + for _, dev := range vdsWithTD { + require.Contains(t, v.Links, httpgwService.ThingLink{ + Href: "/" + dev.deviceID, + Rel: httpgwService.ThingLinkRelationItem, + }) + } +} + +func TestBridgeDeviceGetThings(t *testing.T) { + bridgeDeviceCfg, err := test.GetBridgeDeviceConfig() + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + tearDown := service.SetUp(ctx, t) + defer tearDown() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + httpgwCfg := httpgwTest.MakeConfig(t, true) + shutdownHttp := httpgwTest.New(t, httpgwCfg) + defer shutdownHttp() + + var devIDs []string + for i := 0; i < bridgeDeviceCfg.NumGeneratedBridgedDevices; i++ { + bdName := test.TestBridgeDeviceInstanceName(strconv.Itoa(i)) + bdID := test.MustFindDeviceByName(bdName, func(d *core.Device) deviceCoap.OptionFunc { + return deviceCoap.WithQuery("di=" + d.DeviceID()) + }) + devIDs = append(devIDs, bdID) + bd := bridge.NewDevice(bdID, bdName, bridgeDeviceCfg.NumResourcesPerDevice, true) + shutdownBd := test.OnboardDevice(ctx, t, c, bd, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, bd.GetDefaultResources()) + defer shutdownBd() + } + + rb := httpgwTest.NewRequest(http.MethodGet, httpgwUri.Things, nil).AuthToken(token) + resp := httpgwTest.HTTPDo(t, rb.Build()) + defer func() { + _ = resp.Body.Close() + }() + + var v httpgwService.GetThingsResponse + err = httpgwTest.UnmarshalJson(resp.StatusCode, resp.Body, &v) + require.NoError(t, err) + require.Equal(t, httpgwCfg.UI.WebConfiguration.HTTPGatewayAddress+httpgwUri.Things, v.Base) + require.Len(t, v.Links, bridgeDeviceCfg.NumGeneratedBridgedDevices) + for _, devID := range devIDs { + require.Contains(t, v.Links, httpgwService.ThingLink{ + Href: "/" + devID, + Rel: httpgwService.ThingLinkRelationItem, + }) + } +} + +func getPatchedTD(t *testing.T, deviceCfg bridgeDevice.Config, deviceID string, links []wotTD.IconLinkElement, validateDevices map[string]struct{}, title, host string) *wotTD.ThingDescription { + td, err := bridgeDevice.GetThingDescription(deviceCfg.ThingDescription.File, deviceCfg.NumResourcesPerDevice) + require.NoError(t, err) + + baseURL := host + httpgwUri.API + base, err := url.Parse(baseURL) + require.NoError(t, err) + td.Base = *base + td.Title = title + id, err := bridgeTD.GetThingDescriptionID(deviceID) + require.NoError(t, err) + td.ID = id + + deviceUUID, err := uuid.Parse(deviceID) + require.NoError(t, err) + propertyBaseURL := "/" + httpgwUri.DevicesPathKey + "/" + deviceID + "/" + httpgwUri.ResourcesPathKey + dev, ok := bridgeResourcesTD.GetOCFResourcePropertyElement(schemaDevice.ResourceURI) + require.True(t, ok) + dev, err = bridgeResourcesTD.PatchDeviceResourcePropertyElement(dev, deviceUUID, propertyBaseURL, message.AppJSON, bridgeDevice.DeviceResourceType, httpgwService.CreateHTTPForms) + require.NoError(t, err) + schemaMap := bridgeDevice.GetDataSchemaForAdditionalProperties() + for name, schema := range schemaMap { + dev.Properties.DataSchemaMap[name] = schema + } + td.Properties[schemaDevice.ResourceURI] = dev + + httpgwService.ThingSetSecurity(&td, openid.Config{ + TokenURL: "https://localhost:20009/oauth/token", + AuthURL: "https://localhost:20009/authorize", + }) + + mnt, ok := bridgeResourcesTD.GetOCFResourcePropertyElement(schemaMaintenance.ResourceURI) + require.True(t, ok) + mnt, err = bridgeResourcesTD.PatchMaintenanceResourcePropertyElement(mnt, deviceUUID, propertyBaseURL, message.AppJSON, httpgwService.CreateHTTPForms) + require.NoError(t, err) + td.Properties[schemaMaintenance.ResourceURI] = mnt + + for i := 0; i < deviceCfg.NumResourcesPerDevice; i++ { + href := bridgeDevice.GetTestResourceHref(i) + prop := bridgeDevice.GetPropertyDescriptionForTestResource() + prop, err := bridgeDevice.PatchTestResourcePropertyElement(prop, deviceUUID, propertyBaseURL+href, message.AppJSON, httpgwService.CreateHTTPForms) + require.NoError(t, err) + td.Properties[href] = prop + } + + expectedLinks := make([]wotTD.IconLinkElement, 0, len(links)) + for _, link := range links { + patchedLink, ok := httpgwService.ThingPatchLink(link, validateDevices) + if !ok { + continue + } + expectedLinks = append(expectedLinks, patchedLink) + } + td.Links = expectedLinks + + return &td +} + +func bridgeDeviceInstanceName(idx int) string { + return test.TestBridgeDeviceInstanceName(strconv.Itoa(idx)) +} + +func onboardBridgeDevice(ctx context.Context, t *testing.T, idx int, c pb.GrpcGatewayClient, bridgeDeviceCfg bridgeDevice.Config) (string, func()) { + bdName := bridgeDeviceInstanceName(idx) + bdID := test.MustFindDeviceByName(bdName, func(d *core.Device) deviceCoap.OptionFunc { + return deviceCoap.WithQuery("di=" + d.DeviceID()) + }) + bd := bridge.NewDevice(bdID, bdName, bridgeDeviceCfg.NumResourcesPerDevice, true) + shutdownBd := test.OnboardDevice(ctx, t, c, bd, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, bd.GetDefaultResources()) + return bdID, shutdownBd +} + +func TestBridgeDeviceGetThing(t *testing.T) { + bridgeDeviceCfg, err := test.GetBridgeDeviceConfig() + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + raCfg := raTest.MakeConfig(t) + raCfg.APIs.GRPC.TLS.ClientCertificateRequired = false + tearDown := service.SetUp(ctx, t, service.WithRAConfig(raCfg)) + defer tearDown() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + httpgwCfg := httpgwTest.MakeConfig(t, true) + shutdownHttp := httpgwTest.New(t, httpgwCfg) + defer shutdownHttp() + + deviceIDs := make([]string, 0, 2) + validLinkedDevices := make(map[string]struct{}, 2) + for i := 0; i < 2; i++ { + bdID, shutdownBd := onboardBridgeDevice(ctx, t, i, c, bridgeDeviceCfg) + defer shutdownBd() + deviceIDs = append(deviceIDs, bdID) + validLinkedDevices[bdID] = struct{}{} + } + + // update TD links in resource twin + wotRes, err := c.GetResources(ctx, &pb.GetResourcesRequest{ + ResourceIdFilter: []*pb.ResourceIdFilter{ + { + ResourceId: &commands.ResourceId{ + DeviceId: deviceIDs[0], + Href: bridgeResourcesTD.ResourceURI, + }, + }, + }, + }) + require.NoError(t, err) + resources := make([]*pb.Resource, 0, 1) + for { + res, errR := wotRes.Recv() + if errors.Is(errR, io.EOF) { + break + } + require.NoError(t, errR) + resources = append(resources, res) + } + require.Len(t, resources, 1) + + var updateLinksTD wotTD.ThingDescription + err = json.Decode(resources[0].GetData().GetContent().GetData(), &updateLinksTD) + require.NoError(t, err) + links := []wotTD.IconLinkElement{ + { + Rel: bridgeTD.StringToPtr("icon"), + Href: "https://example.com/icon.png", + }, + { + Rel: bridgeTD.StringToPtr(httpgwService.ThingLinkRelationItem), + Href: "/" + deviceIDs[1], + }, + { + Rel: bridgeTD.StringToPtr(httpgwService.ThingLinkRelationCollection), + Href: "/" + deviceIDs[1], + }, + { + Rel: bridgeTD.StringToPtr(httpgwService.ThingLinkRelationItem), + Href: "/" + uuid.NewString(), + }, + } + updateLinksTD.Links = links + data, err := json.Encode(updateLinksTD) + require.NoError(t, err) + + sub, err := c.SubscribeToEvents(ctx) + require.NoError(t, err) + + err = sub.Send(&pb.SubscribeToEvents{ + Action: &pb.SubscribeToEvents_CreateSubscription_{CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{ + EventFilter: []pb.SubscribeToEvents_CreateSubscription_Event{pb.SubscribeToEvents_CreateSubscription_RESOURCE_CHANGED}, + ResourceIdFilter: []*pb.ResourceIdFilter{ + { + ResourceId: resources[0].GetData().GetResourceId(), + }, + }, + }}, + }) + require.NoError(t, err) + s, err := sub.Recv() + require.NoError(t, err) + require.Equal(t, pb.Event_OperationProcessed_ErrorStatus_OK, s.GetOperationProcessed().GetErrorStatus().GetCode()) + + // overwrite TD links in resource twin + raConn, err := grpc.NewClient(config.RESOURCE_AGGREGATE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = raConn.Close() + }() + raC := raPb.NewResourceAggregateClient(raConn) + _, err = raC.NotifyResourceChanged(ctx, &commands.NotifyResourceChangedRequest{ + ResourceId: resources[0].GetData().GetResourceId(), + Status: commands.Status_OK, + Content: &commands.Content{ + Data: data, + CoapContentFormat: int32(message.AppJSON), + ContentType: message.AppJSON.String(), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: "test", + Sequence: 1, + }, + }) + require.NoError(t, err) + + ev, err := sub.Recv() + require.NoError(t, err) + require.Equal(t, commands.Status_OK, ev.GetResourceChanged().GetStatus()) + + err = sub.CloseSend() + require.NoError(t, err) + + type args struct { + accept string + deviceID string + } + tests := []struct { + name string + args args + want *wotTD.ThingDescription + wantCode int + }{ + { + name: "json: get from resource twin", + args: args{ + deviceID: deviceIDs[0], + }, + want: getPatchedTD(t, bridgeDeviceCfg, deviceIDs[0], links, validLinkedDevices, bridgeDeviceInstanceName(0), httpgwCfg.UI.WebConfiguration.HTTPGatewayAddress), + wantCode: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpgwTest.NewRequest(http.MethodGet, httpgwUri.AliasDeviceThing, nil).AuthToken(token).Accept(tt.args.accept).DeviceId(tt.args.deviceID) + resp := httpgwTest.HTTPDo(t, rb.Build()) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantCode, resp.StatusCode) + values := make([]*wotTD.ThingDescription, 0, 1) + for { + var td wotTD.ThingDescription + err := json.ReadFrom(resp.Body, &td) + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + values = append(values, &td) + } + if tt.wantCode != http.StatusOK { + require.Empty(t, values) + return + } + require.Len(t, values, 1) + test.CmpThingDescription(t, tt.want, values[0]) + }) + } +} diff --git a/http-gateway/service/getWebConfiguration_test.go b/http-gateway/service/getWebConfiguration_test.go index 2bf1019bf..6f3f5d1f0 100644 --- a/http-gateway/service/getWebConfiguration_test.go +++ b/http-gateway/service/getWebConfiguration_test.go @@ -2,7 +2,6 @@ package service_test import ( "context" - "encoding/json" "io" "net/http" "os" @@ -18,19 +17,6 @@ import ( "github.com/stretchr/testify/require" ) -func unmarshalWebConfiguration(code int, input io.Reader, v *httpgwService.WebConfiguration) error { - var data json.RawMessage - err := json.NewDecoder(input).Decode(&data) - if err != nil { - return err - } - if code != http.StatusOK { - return httpgwTest.UnmarshalError(data) - } - err = json.Unmarshal(data, v) - return err -} - func TestRegexpAPI(t *testing.T) { tests := []struct { val string @@ -104,7 +90,7 @@ func TestRequestHandlerGetWebConfiguration(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got httpgwService.WebConfiguration - err := unmarshalWebConfiguration(resp.StatusCode, resp.Body, &got) + err := httpgwTest.UnmarshalJson(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/http-gateway/service/requestHandler.go b/http-gateway/service/requestHandler.go index 6bca657e8..66c15d7c5 100644 --- a/http-gateway/service/requestHandler.go +++ b/http-gateway/service/requestHandler.go @@ -16,14 +16,17 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" "github.com/plgd-dev/hub/v2/pkg/log" kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/pkg/security/openid" pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" ) // RequestHandler for handling incoming request type RequestHandler struct { - client *client.Client - config *Config - mux *runtime.ServeMux + client *client.Client + config *Config + mux *runtime.ServeMux + openIDConfig openid.Config + logger log.Logger } func matchPrefixAndSplitURIPath(requestURI, prefix string) []string { @@ -114,6 +117,19 @@ func resourceEventsMatcher(r *http.Request, rm *mux.RouteMatch) bool { return false } +func thingMatcher(r *http.Request, rm *mux.RouteMatch) bool { + // /api/v1/things/{deviceId} + paths := matchPrefixAndSplitURIPath(r.RequestURI, uri.Things) + if len(paths) == 1 { + if rm.Vars == nil { + rm.Vars = make(map[string]string) + } + rm.Vars[uri.DeviceIDKey] = unescapeString(paths[0]) + return true + } + return false +} + func wsRequestMutator(incoming, outgoing *http.Request) *http.Request { outgoing.Method = http.MethodPost accept := getAccept(incoming) @@ -153,10 +169,12 @@ func (requestHandler *RequestHandler) setupUIHandler(r *mux.Router) { } // NewHTTP returns HTTP handler -func NewRequestHandler(config *Config, r *mux.Router, client *client.Client) (*RequestHandler, error) { +func NewRequestHandler(config *Config, r *mux.Router, client *client.Client, openIDConfig openid.Config, logger log.Logger) (*RequestHandler, error) { requestHandler := &RequestHandler{ - client: client, - config: config, + client: client, + config: config, + openIDConfig: openIDConfig, + logger: logger, mux: serverMux.New( runtime.WithMarshalerOption(ApplicationSubscribeToEventsMIMEWildcard, newSubscribeToEventsMarshaler(serverMux.NewJsonMarshaler())), runtime.WithMarshalerOption(ApplicationSubscribeToEventsProtoJsonContentType, serverMux.NewJsonpbMarshaler()), @@ -173,6 +191,7 @@ func NewRequestHandler(config *Config, r *mux.Router, client *client.Client) (*R r.HandleFunc(uri.AliasDeviceEvents, requestHandler.getEvents).Methods(http.MethodGet) r.HandleFunc(uri.Configuration, requestHandler.getHubConfiguration).Methods(http.MethodGet) r.HandleFunc(uri.HubConfiguration, requestHandler.getHubConfiguration).Methods(http.MethodGet) + r.HandleFunc(uri.Things, requestHandler.getThings).Methods(http.MethodGet) r.PathPrefix(uri.Devices).Methods(http.MethodPost).MatcherFunc(resourceLinksMatcher).HandlerFunc(requestHandler.createResource) r.PathPrefix(uri.Devices).Methods(http.MethodGet).MatcherFunc(resourcePendingCommandsMatcher).HandlerFunc(requestHandler.getResourcePendingCommands) @@ -181,6 +200,8 @@ func NewRequestHandler(config *Config, r *mux.Router, client *client.Client) (*R r.PathPrefix(uri.Devices).Methods(http.MethodPut).MatcherFunc(resourceMatcher).HandlerFunc(requestHandler.updateResource) r.PathPrefix(uri.Devices).Methods(http.MethodGet).MatcherFunc(resourceEventsMatcher).HandlerFunc(requestHandler.getEvents) + r.PathPrefix(uri.Things).Methods(http.MethodGet).MatcherFunc(thingMatcher).HandlerFunc(requestHandler.getThing) + // register grpc-proxy handler if err := pb.RegisterGrpcGatewayHandlerClient(context.Background(), requestHandler.mux, requestHandler.client.GrpcGatewayClient()); err != nil { return nil, fmt.Errorf("failed to register grpc-gateway handler: %w", err) diff --git a/http-gateway/service/service.go b/http-gateway/service/service.go index b18d9ffee..8f4161b2b 100644 --- a/http-gateway/service/service.go +++ b/http-gateway/service/service.go @@ -86,7 +86,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg }) grpcClient := pb.NewGrpcGatewayClient(grpcConn.GRPC()) client := client.New(grpcClient) - _, err = NewRequestHandler(&config, s.GetRouter(), client) + _, err = NewRequestHandler(&config, s.GetRouter(), client, validator.OpenIDConfiguration(), logger) if err != nil { var errors *multierror.Error errors = multierror.Append(errors, fmt.Errorf("cannot create request handler: %w", err)) diff --git a/http-gateway/service/updateResource.go b/http-gateway/service/updateResource.go index 332930a18..e47c06cbf 100644 --- a/http-gateway/service/updateResource.go +++ b/http-gateway/service/updateResource.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "github.com/gorilla/mux" "github.com/plgd-dev/go-coap/v3/message" @@ -53,5 +54,14 @@ func (requestHandler *RequestHandler) updateResource(w http.ResponseWriter, r *h } r.Body = newBody - requestHandler.mux.ServeHTTP(w, r) + rec := httptest.NewRecorder() + onlyContent := r.URL.Query().Get(uri.OnlyContentQueryKey) + requestHandler.mux.ServeHTTP(rec, r) + allowEmptyContent := false + if parseBoolQuery(onlyContent) { + allowEmptyContent = requestHandler.filterOnlyContent(rec, "data", "content") + } + toSimpleResponse(w, rec, allowEmptyContent, func(w http.ResponseWriter, err error) { + serverMux.WriteError(w, kitNetGrpc.ForwardErrorf(codes.InvalidArgument, "cannot update resource('/%v%v') from the device: %v", deviceID, href, err)) + }, streamResponseKey) } diff --git a/http-gateway/service/updateResource_test.go b/http-gateway/service/updateResource_test.go index 06b5502c7..19609f3e9 100644 --- a/http-gateway/service/updateResource_test.go +++ b/http-gateway/service/updateResource_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/plgd-dev/device/v2/pkg/codec/json" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/go-coap/v3/message" @@ -38,6 +39,7 @@ func TestRequestHandlerUpdateResourcesValues(t *testing.T) { deviceID string resourceHref string resourceInterface string + onlyContent bool resourceData interface{} ttl time.Duration } @@ -54,6 +56,7 @@ func TestRequestHandlerUpdateResourcesValues(t *testing.T) { accept: uri.ApplicationProtoJsonContentType, deviceID: deviceID, resourceHref: "/unknown", + onlyContent: true, }, wantErr: true, wantHTTPCode: http.StatusNotFound, @@ -211,7 +214,7 @@ func TestRequestHandlerUpdateResourcesValues(t *testing.T) { require.NoError(t, err) rb := httpgwTest.NewRequest(http.MethodPut, uri.AliasDeviceResource, bytes.NewReader(data)).AuthToken(token).Accept(tt.args.accept) rb.DeviceId(tt.args.deviceID).ResourceHref(tt.args.resourceHref).ResourceInterface(tt.args.resourceInterface).ContentType(tt.args.contentType) - rb.AddTimeToLive(tt.args.ttl) + rb.AddTimeToLive(tt.args.ttl).OnlyContent(tt.args.onlyContent) resp := httpgwTest.HTTPDo(t, rb.Build()) defer func() { _ = resp.Body.Close() @@ -229,3 +232,104 @@ func TestRequestHandlerUpdateResourcesValues(t *testing.T) { }) } } + +func TestRequestHandlerUpdateResourcesValuesWithOnlyContent(t *testing.T) { + deviceID := test.MustFindDeviceByName(test.TestDeviceName) + switchID := "1" + type args struct { + accept string + contentType string + deviceID string + resourceHref string + resourceData interface{} + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + wantHTTPCode int + }{ + { + name: "valid - accept " + uri.ApplicationProtoJsonContentType, + args: args{ + accept: uri.ApplicationProtoJsonContentType, + contentType: message.AppJSON.String(), + deviceID: deviceID, + resourceHref: test.TestResourceLightInstanceHref("1"), + resourceData: map[string]interface{}{ + "power": 102, + }, + }, + want: nil, + wantHTTPCode: http.StatusOK, + }, + { + name: "revert-update - accept empty", + args: args{ + contentType: message.AppJSON.String(), + deviceID: deviceID, + resourceHref: test.TestResourceLightInstanceHref("1"), + resourceData: map[string]interface{}{ + "power": 0, + }, + }, + want: nil, + wantHTTPCode: http.StatusOK, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := service.SetUp(ctx, t) + defer tearDown() + + shutdownHttp := httpgwTest.SetUp(t) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + _, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + test.AddDeviceSwitchResources(ctx, t, deviceID, c, switchID) + time.Sleep(200 * time.Millisecond) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpgwTest.GetContentData(&pb.Content{ + Data: test.EncodeToCbor(t, tt.args.resourceData), + ContentType: message.AppOcfCbor.String(), + }, tt.args.contentType) + require.NoError(t, err) + rb := httpgwTest.NewRequest(http.MethodPut, uri.AliasDeviceResource, bytes.NewReader(data)).AuthToken(token).Accept(tt.args.accept) + rb.DeviceId(tt.args.deviceID).ResourceHref(tt.args.resourceHref).ContentType(tt.args.contentType) + rb.OnlyContent(true) + resp := httpgwTest.HTTPDo(t, rb.Build()) + defer func() { + _ = resp.Body.Close() + }() + assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got interface{} + err = json.ReadFrom(resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/http-gateway/swagger.yaml b/http-gateway/swagger.yaml index c43d8b28a..e07d35627 100644 --- a/http-gateway/swagger.yaml +++ b/http-gateway/swagger.yaml @@ -389,6 +389,7 @@ paths: - $ref: "#/components/parameters/shadow" - $ref: "#/components/parameters/timeToLive" - $ref: "#/components/parameters/etag" + - $ref: "#/components/parameters/onlyContent" responses: 200: description: "Resource content." @@ -419,6 +420,7 @@ paths: parameters: - $ref: "#/components/parameters/interface" - $ref: "#/components/parameters/timeToLive" + - $ref: "#/components/parameters/onlyContent" requestBody: description: "Updated content of the resource." content: @@ -1762,6 +1764,13 @@ components: schema: type: string format: base64 + onlyContent: + name: onlyContent + in: query + description: "Return only content of the resource in the response." + schema: + type: boolean + default: false responses: ok: description: Content is stored in body. diff --git a/http-gateway/test/http.go b/http-gateway/test/http.go index 466a248a0..ad47b5b3c 100644 --- a/http-gateway/test/http.go +++ b/http-gateway/test/http.go @@ -114,6 +114,11 @@ func (c *RequestBuilder) Accept(accept string) *RequestBuilder { return c } +func (c *RequestBuilder) OnlyContent(v bool) *RequestBuilder { + c.AddQuery(uri.OnlyContentQueryKey, strconv.FormatBool(v)) + return c +} + func (c *RequestBuilder) ContentType(contentType string) *RequestBuilder { if contentType == "" { return c diff --git a/http-gateway/test/test.go b/http-gateway/test/test.go index 2e7b7dd79..8c63f9714 100644 --- a/http-gateway/test/test.go +++ b/http-gateway/test/test.go @@ -2,6 +2,9 @@ package test import ( "context" + "encoding/json" + "io" + "net/http" "os" "sync" "time" @@ -11,7 +14,7 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/test/config" testHttp "github.com/plgd-dev/hub/v2/test/http" "github.com/plgd-dev/kit/v2/codec/cbor" @@ -53,7 +56,7 @@ func MakeConfig(t require.TestingT, enableUI bool) service.Config { cfg.APIs.HTTP.Server = config.MakeHttpServerConfig() cfg.Clients.GrpcGateway.Connection = config.MakeGrpcClientConfig(config.GRPC_GW_HOST) - cfg.Clients.OpenTelemetryCollector = http.OpenTelemetryCollectorConfig{ + cfg.Clients.OpenTelemetryCollector = pkgHttp.OpenTelemetryCollectorConfig{ Config: config.MakeOpenTelemetryCollectorClient(), } @@ -111,3 +114,16 @@ func GetContentData(content *pb.Content, desiredContentType string) ([]byte, err } return []byte(v), err } + +func UnmarshalJson(code int, input io.Reader, v any) error { + var data json.RawMessage + err := json.NewDecoder(input).Decode(&data) + if err != nil { + return err + } + if code != http.StatusOK { + return UnmarshalError(data) + } + err = json.Unmarshal(data, v) + return err +} diff --git a/http-gateway/uri/uri.go b/http-gateway/uri/uri.go index b20a5e6e8..54e53ab6f 100644 --- a/http-gateway/uri/uri.go +++ b/http-gateway/uri/uri.go @@ -21,6 +21,7 @@ const ( TimestampFilterQueryKey = "timestampFilter" CorrelationIdFilterQueryKey = "correlationIdFilter" ETagQueryKey = "etag" + OnlyContentQueryKey = "onlyContent" AliasInterfaceQueryKey = "interface" AliasCommandFilterQueryKey = "command" @@ -34,11 +35,13 @@ const ( AcceptHeaderKey = "Accept" ETagHeaderKey = "ETag" + DevicesPathKey = "devices" ResourcesPathKey = "resources" ResourceLinksPathKey = "resource-links" PendingCommandsPathKey = "pending-commands" PendingMetadataUpdatesPathKey = "pending-metadata-updates" EventsPathKey = "events" + ThingsPathKey = "things" ApplicationProtoJsonContentType = "application/protojson" @@ -53,6 +56,11 @@ const ( // web configuration for ui WebConfiguration = "/web_configuration.json" + // list devices with thing descriptions + Things = API + "/" + ThingsPathKey + // (HTTP ALIAS) GET /api/v1/things/{deviceId} + AliasDeviceThing = Things + "/{" + DeviceIDKey + "}" + // (GRPC + HTTP) GET /api/v1/devices -> rpc GetDevices // (GRPC + HTTP) DELETE /api/v1/devices -> rpc DeleteDevices Devices = API + "/devices" @@ -133,4 +141,5 @@ var QueryCaseInsensitive = map[string]string{ strings.ToLower(TimestampFilterQueryKey): TimestampFilterQueryKey, strings.ToLower(TimeToLiveQueryKey): TimeToLiveQueryKey, strings.ToLower(CorrelationIdFilterQueryKey): CorrelationIdFilterQueryKey, + strings.ToLower(OnlyContentQueryKey): OnlyContentQueryKey, } diff --git a/test/bridge-device/bridge-device.jsonld b/test/bridge-device/bridge-device.jsonld new file mode 100644 index 000000000..4b72a02b6 --- /dev/null +++ b/test/bridge-device/bridge-device.jsonld @@ -0,0 +1,8 @@ +{ + "@context": "https://www.w3.org/2019/wot/td/v1", + "@type": [ + "Thing" + ], + "id": "urn:uuid:bridge-test", + "properties": {} +} \ No newline at end of file diff --git a/test/bridge-device/config.yaml b/test/bridge-device/config.yaml new file mode 100644 index 000000000..5be18ae10 --- /dev/null +++ b/test/bridge-device/config.yaml @@ -0,0 +1,19 @@ +apis: + coap: + id: 8f596b43-29c0-4147-8b40-e99268ab30f7 + name: "bridge-test" + externalAddresses: + - "127.0.0.1:15683" + - "[::1]:15683" + maxMessageSize: 2097152 +log: + level: "info" +cloud: + enabled: true +credential: + enabled: true +thingDescription: + enabled: true + file: "bridge-device.jsonld" +numGeneratedBridgedDevices: 3 +numResourcesPerDevice: 3 diff --git a/test/cloud-server/Dockerfile b/test/cloud-server/Dockerfile index ca78855ee..a4542f439 100644 --- a/test/cloud-server/Dockerfile +++ b/test/cloud-server/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.20.14-alpine AS build +FROM golang:1.22.3-alpine AS build ARG VERSION ARG COMMIT_DATE ARG SHORT_COMMIT diff --git a/test/device/bridge/device.go b/test/device/bridge/device.go index 46e9f2967..80af77d79 100644 --- a/test/device/bridge/device.go +++ b/test/device/bridge/device.go @@ -21,6 +21,8 @@ package bridge import ( "time" + "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" "github.com/plgd-dev/device/v2/schema" schemaDevice "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" @@ -32,10 +34,10 @@ import ( var TestResources = []schema.ResourceLink{ { Href: schemaDevice.ResourceURI, - ResourceTypes: []string{"oic.d.virtual", schemaDevice.ResourceType}, + ResourceTypes: []string{bridgeDevice.DeviceResourceType, schemaDevice.ResourceType}, Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}, Policy: &schema.Policy{ - BitMask: 1, + BitMask: schema.Discoverable, }, }, { @@ -43,21 +45,29 @@ var TestResources = []schema.ResourceLink{ ResourceTypes: []string{maintenance.ResourceType}, Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}, Policy: &schema.Policy{ - BitMask: 1, + BitMask: schema.Discoverable, }, }, } type Device struct { device.BaseDevice + testResources int // number of test resources + tdEnabled bool // thingDescription resource enabled } -func NewDevice(id, name string) *Device { +func NewDevice(id, name string, testResources int, tdEnabled bool) *Device { return &Device{ - BaseDevice: device.MakeBaseDevice(id, name), + BaseDevice: device.MakeBaseDevice(id, name), + testResources: testResources, + tdEnabled: tdEnabled, } } +func (d *Device) GetType() device.Type { + return device.Bridged +} + func (d *Device) GetSDKClientOptions() []sdk.Option { return []sdk.Option{sdk.WithUseDeviceIDInQuery(true)} } @@ -67,5 +77,26 @@ func (d *Device) GetRetryInterval(int) time.Duration { } func (d *Device) GetDefaultResources() schema.ResourceLinks { - return TestResources + testResources := TestResources + for i := 0; i < d.testResources; i++ { + testResources = append(testResources, schema.ResourceLink{ + Href: bridgeDevice.GetTestResourceHref(i), + ResourceTypes: []string{bridgeDevice.TestResourceType}, + Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}, + Policy: &schema.Policy{ + BitMask: schema.Discoverable | schema.Observable, + }, + }) + } + if d.tdEnabled { + testResources = append(testResources, schema.ResourceLink{ + Href: thingDescription.ResourceURI, + ResourceTypes: []string{thingDescription.ResourceType}, + Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}, + Policy: &schema.Policy{ + BitMask: schema.Discoverable | schema.Observable, + }, + }) + } + return testResources } diff --git a/test/device/device.go b/test/device/device.go index 28ccb9933..de3e9d92c 100644 --- a/test/device/device.go +++ b/test/device/device.go @@ -33,7 +33,17 @@ import ( "github.com/plgd-dev/hub/v2/test/sdk" ) +type Type int + +const ( + OCF Type = iota + Bridged +) + type Device interface { + // GetType returns device type + GetType() Type + // GetID returns device ID GetID() string @@ -81,14 +91,9 @@ func (bd *BaseDevice) GetSDKClientOptions() []sdk.Option { return nil } -type Type int +type GetResourceOpts func(*core.Device) deviceCoap.OptionFunc -const ( - OCF Type = iota - Bridged -) - -func FindDeviceByName(ctx context.Context, name string, getResourceOpts func(*core.Device) deviceCoap.OptionFunc) (deviceID string, _ error) { +func FindDeviceByName(ctx context.Context, name string, getResourceOpts ...GetResourceOpts) (deviceID string, _ error) { client := core.NewClient() ctx, cancel := context.WithCancel(ctx) @@ -115,7 +120,7 @@ type findDeviceIDByNameHandler struct { id atomic.Value name string cancel context.CancelFunc - getResourceOptions func(*core.Device) deviceCoap.OptionFunc + getResourceOptions []GetResourceOpts } func (h *findDeviceIDByNameHandler) Handle(ctx context.Context, dev *core.Device) { @@ -136,7 +141,9 @@ func (h *findDeviceIDByNameHandler) Handle(ctx context.Context, dev *core.Device var d device.Device var getResourceOpts []deviceCoap.OptionFunc if h.getResourceOptions != nil { - getResourceOpts = append(getResourceOpts, h.getResourceOptions(dev)) + for _, opts := range h.getResourceOptions { + getResourceOpts = append(getResourceOpts, opts(dev)) + } } err = dev.GetResource(ctx, l, &d, getResourceOpts...) if err != nil { diff --git a/test/device/ocf/device.go b/test/device/ocf/device.go index 495377798..3000bfea4 100644 --- a/test/device/ocf/device.go +++ b/test/device/ocf/device.go @@ -119,6 +119,10 @@ func NewDevice(id, name string) *Device { } } +func (d *Device) GetType() device.Type { + return device.OCF +} + func (d *Device) GetRetryInterval(attempt int) time.Duration { /* [2s, 4s, 8s, 16s, 32s, 64s] */ return time.Duration(math.Exp2(float64(attempt))) * time.Second diff --git a/test/test.go b/test/test.go index cf9a5b146..605679d74 100644 --- a/test/test.go +++ b/test/test.go @@ -2,6 +2,7 @@ package test import ( "context" + "errors" "fmt" "net" "os" @@ -13,6 +14,7 @@ import ( deviceClient "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/client/core" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" deviceCoap "github.com/plgd-dev/device/v2/pkg/net/coap" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/acl" @@ -200,6 +202,10 @@ func TestResourceSwitchesInstanceHref(id string) string { return TestResourceSwitchesHref + "/" + id } +func TestBridgeDeviceInstanceName(id string) string { + return "bridged-device-" + id +} + type LightResourceRepresentation struct { Name string `json:"name"` Power uint64 `json:"power"` @@ -570,7 +576,7 @@ func OnboardDeviceForClient(ctx context.Context, t *testing.T, c pb.GrpcGatewayC } CheckProtobufs(t, expectedEvent, ev, RequireToCheckFunc(require.Equal)) onboard() - WaitForDevice(t, subClient, d.GetID(), ev.GetSubscriptionId(), ev.GetCorrelationId(), expectedResources) + WaitForDevice(t, subClient, d, ev.GetSubscriptionId(), ev.GetCorrelationId(), expectedResources) err = subClient.CloseSend() require.NoError(t, err) } else { @@ -606,7 +612,7 @@ func OffBoardDevSim(ctx context.Context, t *testing.T, deviceID string) { OffboardDevice(ctx, t, ocf.NewDevice(deviceID, TestDeviceName)) } -func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, deviceID, subID, correlationID string, expectedResources []schema.ResourceLink) { +func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, dev device.Device, subID, correlationID string, expectedResources []schema.ResourceLink) { getID := func(ev *pb.Event) string { switch v := ev.GetType().(type) { case *pb.Event_DeviceRegistered_: @@ -638,7 +644,7 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, val.DeviceMetadataUpdated.GetConnection().ConnectedAt = 0 require.NotEmpty(t, val.DeviceMetadataUpdated.GetConnection().GetServiceId()) val.DeviceMetadataUpdated.GetConnection().ServiceId = "" - if TestDeviceType != device.Bridged && !config.COAP_GATEWAY_UDP_ENABLED { + if dev.GetType() != device.Bridged && !config.COAP_GATEWAY_UDP_ENABLED { // TODO: fix bug in iotivity-lite, for DTLS it is not fill endpoints require.NotEmpty(t, val.DeviceMetadataUpdated.GetConnection().GetLocalEndpoints()) } @@ -675,7 +681,7 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, CorrelationId: correlationID, Type: &pb.Event_DeviceRegistered_{ DeviceRegistered: &pb.Event_DeviceRegistered{ - DeviceIds: []string{deviceID}, + DeviceIds: []string{dev.GetID()}, EventMetadata: &isEvents.EventMetadata{ HubId: config.HubID(), }, @@ -695,7 +701,7 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, CorrelationId: correlationID, Type: &pb.Event_DeviceMetadataUpdated{ DeviceMetadataUpdated: &events.DeviceMetadataUpdated{ - DeviceId: deviceID, + DeviceId: dev.GetID(), Connection: &commands.Connection{ Status: commands.Connection_ONLINE, Protocol: StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), @@ -723,7 +729,7 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, CorrelationId: correlationID, Type: &pb.Event_DeviceMetadataUpdated{ DeviceMetadataUpdated: &events.DeviceMetadataUpdated{ - DeviceId: deviceID, + DeviceId: dev.GetID(), Connection: &commands.Connection{ Status: commands.Connection_ONLINE, Protocol: StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), @@ -751,7 +757,7 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, CorrelationId: correlationID, Type: &pb.Event_DeviceMetadataUpdated{ DeviceMetadataUpdated: &events.DeviceMetadataUpdated{ - DeviceId: deviceID, + DeviceId: dev.GetID(), Connection: &commands.Connection{ Status: commands.Connection_ONLINE, Protocol: StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), @@ -768,8 +774,8 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, CorrelationId: correlationID, Type: &pb.Event_ResourcePublished{ ResourcePublished: &events.ResourceLinksPublished{ - DeviceId: deviceID, - Resources: ResourceLinksToResources(deviceID, expectedResources), + DeviceId: dev.GetID(), + Resources: ResourceLinksToResources(dev.GetID(), expectedResources), }, }, }, @@ -777,14 +783,14 @@ func WaitForDevice(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, for _, r := range expectedResources { expectedEvents[getID(&pb.Event{Type: &pb.Event_ResourceChanged{ ResourceChanged: &events.ResourceChanged{ - ResourceId: commands.NewResourceID(deviceID, r.Href), + ResourceId: commands.NewResourceID(dev.GetID(), r.Href), }, }})] = &pb.Event{ SubscriptionId: subID, CorrelationId: correlationID, Type: &pb.Event_ResourceChanged{ ResourceChanged: &events.ResourceChanged{ - ResourceId: commands.NewResourceID(deviceID, r.Href), + ResourceId: commands.NewResourceID(dev.GetID(), r.Href), Status: commands.Status_OK, ResourceTypes: r.ResourceTypes, }, @@ -824,12 +830,12 @@ func MustGetHostname() string { return n } -func MustFindDeviceByName(name string) (deviceID string) { +func MustFindDeviceByName(name string, getResourceOpts ...device.GetResourceOpts) (deviceID string) { var err error for i := 0; i < 3; i++ { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - deviceID, err = device.FindDeviceByName(ctx, name, nil) + deviceID, err = device.FindDeviceByName(ctx, name, getResourceOpts...) if err == nil { return deviceID } @@ -837,12 +843,20 @@ func MustFindDeviceByName(name string) (deviceID string) { panic(err) } +func GetBridgeDeviceConfig() (bridgeDevice.Config, error) { + cfgFile := os.Getenv("TEST_BRIDGE_DEVICE_CONFIG") + if cfgFile == "" { + return bridgeDevice.Config{}, errors.New("TEST_BRIDGE_DEVICE_CONFIG not set") + } + return bridgeDevice.LoadConfig(cfgFile) +} + func MustFindTestDevice() device.Device { - var getResourceOpts func(*core.Device) deviceCoap.OptionFunc + var getResourceOpts []device.GetResourceOpts if TestDeviceType == device.Bridged { - getResourceOpts = func(d *core.Device) deviceCoap.OptionFunc { + getResourceOpts = append(getResourceOpts, func(d *core.Device) deviceCoap.OptionFunc { return deviceCoap.WithQuery("di=" + d.DeviceID()) - } + }) } var deviceID string @@ -850,7 +864,7 @@ func MustFindTestDevice() device.Device { for i := 0; i < 3; i++ { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - deviceID, err = device.FindDeviceByName(ctx, TestDeviceName, getResourceOpts) + deviceID, err = device.FindDeviceByName(ctx, TestDeviceName, getResourceOpts...) if err == nil { break } @@ -861,7 +875,11 @@ func MustFindTestDevice() device.Device { } if TestDeviceType == device.Bridged { - return bridge.NewDevice(deviceID, TestDeviceName) + bridgeDeviceCfg, err := GetBridgeDeviceConfig() + if err != nil { + panic(err) + } + return bridge.NewDevice(deviceID, TestDeviceName, bridgeDeviceCfg.NumResourcesPerDevice, true) } return ocf.NewDevice(deviceID, TestDeviceName) } diff --git a/test/thingDescription.go b/test/thingDescription.go new file mode 100644 index 000000000..bbe9e8a11 --- /dev/null +++ b/test/thingDescription.go @@ -0,0 +1,33 @@ +package test + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/require" + wotTD "github.com/web-of-things-open-source/thingdescription-go/thingDescription" +) + +func CmpThingDescription(t *testing.T, expected, got *wotTD.ThingDescription) { + sort.Strings(expected.Type.StringArray) + sort.Strings(got.Type.StringArray) + sortProperties := func(td *wotTD.ThingDescription) { + for key, prop := range td.Properties { + for idx, form := range prop.Forms { + if form.Op.StringArray == nil { + continue + } + sort.Strings(form.Op.StringArray) + prop.Forms[idx] = form + } + if prop.Type == nil { + continue + } + sort.Strings(prop.Type.StringArray) + td.Properties[key] = prop + } + } + sortProperties(expected) + sortProperties(got) + require.Equal(t, expected, got) +} diff --git a/test/virtual-device/virtualDevice.go b/test/virtual-device/virtualDevice.go index cb7d87378..54b23096a 100644 --- a/test/virtual-device/virtualDevice.go +++ b/test/virtual-device/virtualDevice.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" @@ -29,7 +30,7 @@ import ( "google.golang.org/grpc/status" ) -func CreateDeviceResourceLinks(deviceID string, numResources int) []*commands.Resource { +func CreateDeviceResourceLinks(deviceID string, numResources int, tdEnabled bool) []*commands.Resource { resources := make([]*commands.Resource, 0, numResources) for i := 0; i < numResources; i++ { resources = append(resources, &commands.Resource{ @@ -60,10 +61,23 @@ func CreateDeviceResourceLinks(deviceID string, numResources int) []*commands.Re BitFlags: int32(schema.Observable | schema.Discoverable), }, }) + + if tdEnabled { + resources = append(resources, &commands.Resource{ + Href: thingDescription.ResourceURI, + DeviceId: deviceID, + ResourceTypes: []string{thingDescription.ResourceType}, + Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}, + Policy: &commands.Policy{ + BitFlags: int32(schema.Discoverable), + }, + }) + } + return resources } -func CreateDevice(ctx context.Context, t *testing.T, name string, deviceID string, numResources int, protocol commands.Connection_Protocol, isClient pb.IdentityStoreClient, raClient raPb.ResourceAggregateClient) { +func CreateDevice(ctx context.Context, t *testing.T, name string, deviceID string, numResources int, tdEnabled bool, protocol commands.Connection_Protocol, isClient pb.IdentityStoreClient, raClient raPb.ResourceAggregateClient) { const connID = "conn-Id" var conSeq uint64 incSeq := func() uint64 { @@ -105,7 +119,7 @@ func CreateDevice(ctx context.Context, t *testing.T, name string, deviceID strin assert.NoError(t, err) //nolint:testifylint } - resources := CreateDeviceResourceLinks(deviceID, numResources) + resources := CreateDeviceResourceLinks(deviceID, numResources, tdEnabled) pub := commands.PublishResourceLinksRequest{ DeviceId: deviceID, Resources: resources, @@ -223,7 +237,7 @@ func CreateDevices(ctx context.Context, t *testing.T, numDevices int, numResourc err = sem.Acquire(ctx, 1) require.NoError(t, err) go func(i int) { - CreateDevice(ctx, t, fmt.Sprintf("dev-%v", i), uuid.NewString(), numResourcesPerDevice, protocol, isClient, raClient) + CreateDevice(ctx, t, fmt.Sprintf("dev-%v", i), uuid.NewString(), numResourcesPerDevice, false, protocol, isClient, raClient) sem.Release(1) }(i) } diff --git a/tools/cert-tool/Dockerfile b/tools/cert-tool/Dockerfile index 370f7d98b..73dd69940 100644 --- a/tools/cert-tool/Dockerfile +++ b/tools/cert-tool/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.14-alpine AS build +FROM golang:1.22.3-alpine AS build ARG DIRECTORY ARG NAME ARG VERSION diff --git a/tools/docker/Dockerfile.in b/tools/docker/Dockerfile.in index 50782a0a8..46e1e79b4 100644 --- a/tools/docker/Dockerfile.in +++ b/tools/docker/Dockerfile.in @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.20.14-alpine AS build +FROM golang:1.22.3-alpine AS build ARG VERSION ARG COMMIT_DATE ARG SHORT_COMMIT