From f00547a77914f1f7aeff04437cc892b57f11edbd Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Mon, 29 Jan 2024 14:38:25 +0100 Subject: [PATCH] bridge: test device with mocked coap-gateway --- .codecov.yml | 2 +- .vscode/settings.json | 3 + Makefile | 65 ++++- bridge/device/cloud/manager.go | 11 +- bridge/device/cloud/manager_test.go | 110 ++++++++ bridge/device/cloud/publishResources.go | 13 +- bridge/device/cloud/refreshToken.go | 21 +- bridge/device/cloud/signIn.go | 24 +- bridge/device/cloud/signOff.go | 3 +- bridge/device/cloud/signUp.go | 29 +-- bridge/device/config.go | 1 + bridge/getDevices_test.go | 8 +- bridge/getResource_test.go | 8 +- bridge/observeResource_test.go | 8 +- bridge/onboardDevice_test.go | 8 +- bridge/test/test.go | 46 +++- bridge/updateResource_test.go | 8 +- pkg/ocf/cloud/request.go | 74 ++++++ {bridge/device => pkg/ocf}/cloud/uri.go | 2 +- sonar-project.properties | 4 + test/coap-gateway/defaultHandler.go | 240 ++++++++++++++++++ test/coap-gateway/service/client.go | 129 ++++++++++ test/coap-gateway/service/config.go | 31 +++ test/coap-gateway/service/refreshToken.go | 52 ++++ .../coap-gateway/service/resourceDirectory.go | 131 ++++++++++ test/coap-gateway/service/service.go | 238 +++++++++++++++++ test/coap-gateway/service/serviceHandler.go | 65 +++++ test/coap-gateway/service/signIn.go | 95 +++++++ test/coap-gateway/service/signUp.go | 96 +++++++ test/coap-gateway/test.go | 69 +++++ test/ocfbridge/main.go | 4 +- test/test.go | 27 +- 32 files changed, 1518 insertions(+), 107 deletions(-) create mode 100644 bridge/device/cloud/manager_test.go create mode 100644 pkg/ocf/cloud/request.go rename {bridge/device => pkg/ocf}/cloud/uri.go (96%) create mode 100644 test/coap-gateway/defaultHandler.go create mode 100644 test/coap-gateway/service/client.go create mode 100644 test/coap-gateway/service/config.go create mode 100644 test/coap-gateway/service/refreshToken.go create mode 100644 test/coap-gateway/service/resourceDirectory.go create mode 100644 test/coap-gateway/service/service.go create mode 100644 test/coap-gateway/service/serviceHandler.go create mode 100644 test/coap-gateway/service/signIn.go create mode 100644 test/coap-gateway/service/signUp.go create mode 100644 test/coap-gateway/test.go diff --git a/.codecov.yml b/.codecov.yml index 7ad5f5b9..91f8aa14 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,6 @@ ignore: - "client/app/app.go" - - "cmd/ocfclient/ocfclient.go" + - "cmd/ocfclient/*.go" - "cmd/ocfbridge/*.go" - "**/main.go" - "**/test/**/*.go" diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f77fbf6..08548263 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,9 @@ "MFG_KEY": "${workspaceFolder}/.tmp/pki_certs/mfgkey.pem", "IDENTITY_CRT": "${workspaceFolder}/.tmp/pki_certs/identitycrt.pem", "IDENTITY_KEY": "${workspaceFolder}/.tmp/pki_certs/identitykey.pem", + "COAP_CRT": "${workspaceFolder}/.tmp/pki_certs/coapcrt.pem", + "COAP_KEY": "${workspaceFolder}/.tmp/pki_certs/coapkey.pem", + "CLOUD_ID": "adebc667-1f2b-41e3-bf5c-6d6eabc68cc6", }, "files.watcherExclude": { "**/plgd-dev/device/v2/**": true diff --git a/Makefile b/Makefile index 13512e78..b8215b81 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,11 @@ SHELL = /bin/bash SERVICE_NAME = cloud-server-test VERSION_TAG = vnext-$(shell git rev-parse --short=7 --verify HEAD) SIMULATOR_NAME_SUFFIX ?= $(shell hostname) +USER_ID := $(shell id -u) +GROUP_ID := $(shell id -g) TMP_PATH = $(shell pwd)/.tmp CERT_PATH = $(TMP_PATH)/pki_certs +CLOUD_SID ?= adebc667-1f2b-41e3-bf5c-6d6eabc68cc6 DEVSIM_NET_HOST_PATH = $(shell pwd)/.tmp/devsim-net-host CERT_TOOL_IMAGE ?= ghcr.io/plgd-dev/hub/cert-tool:vnext # supported values: ECDSA-SHA256, ECDSA-SHA384, ECDSA-SHA512 @@ -35,22 +38,52 @@ INTERMEDIATE_CA_CRT = $(CERT_PATH)/intermediatecacrt.pem INTERMEDIATE_CA_KEY = $(CERT_PATH)/intermediatecakey.pem MFG_CRT = $(CERT_PATH)/mfgcrt.pem MFG_KEY = $(CERT_PATH)/mfgkey.pem +COAP_CRT = $(CERT_PATH)/coapcrt.pem +COAP_KEY = $(CERT_PATH)/coapkey.pem certificates: mkdir -p $(CERT_PATH) chmod 0777 $(CERT_PATH) docker pull $(CERT_TOOL_IMAGE) - docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --outCert=/out/cloudca.pem --outKey=/out/cloudcakey.pem \ - --cert.subject.cn="ca" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ - --cmd.generateRootCA - docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \ - --outCert=/out/intermediatecacrt.pem --outKey=/out/intermediatecakey.pem --cert.basicConstraints.maxPathLen=0 \ - --cert.subject.cn="intermediateCA" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \ - --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cmd.generateIntermediateCA - docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --signerCert=/out/intermediatecacrt.pem \ - --signerKey=/out/intermediatecakey.pem --outCert=/out/mfgcrt.pem --outKey=/out/mfgkey.pem --cert.san.domain=localhost \ - --cert.san.ip=127.0.0.1 --cert.subject.cn="mfg" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \ - --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cmd.generateCertificate + + docker run \ + --rm -v $(CERT_PATH):/out \ + --user $(USER_ID):$(GROUP_ID) \ + $(CERT_TOOL_IMAGE) \ + --outCert=/out/cloudca.pem --outKey=/out/cloudcakey.pem \ + --cert.subject.cn="ca" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ + --cmd.generateRootCA + + docker run \ + --rm -v $(CERT_PATH):/out \ + --user $(USER_ID):$(GROUP_ID) \ + $(CERT_TOOL_IMAGE) \ + --signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \ + --outCert=/out/intermediatecacrt.pem --outKey=/out/intermediatecakey.pem \ + --cert.basicConstraints.maxPathLen=0 --cert.subject.cn="intermediateCA" \ + --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \ + --cmd.generateIntermediateCA + + docker run \ + --rm -v $(CERT_PATH):/out \ + --user $(USER_ID):$(GROUP_ID) \ + $(CERT_TOOL_IMAGE) \ + --signerCert=/out/intermediatecacrt.pem --signerKey=/out/intermediatecakey.pem \ + --outCert=/out/mfgcrt.pem --outKey=/out/mfgkey.pem --cert.san.domain=localhost \ + --cert.san.ip=127.0.0.1 --cert.subject.cn="mfg" \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ + --cmd.generateCertificate + + docker run \ + --rm -v $(CERT_PATH):/out \ + --user $(USER_ID):$(GROUP_ID) \ + ${CERT_TOOL_IMAGE} \ + --signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \ + --outCert=/out/coapcrt.pem --outKey=/out/coapkey.pem \ + --cert.san.ip=127.0.0.1 --cert.san.domain=localhost \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ + --cmd.generateCertificate --cert.subject.cn=uuid:$(CLOUD_SID) + sudo chown -R $(shell whoami) $(CERT_PATH) chmod -R 0777 $(CERT_PATH) @@ -72,9 +105,15 @@ env: clean certificates unit-test: certificates mkdir -p $(TMP_PATH) - ROOT_CA_CRT="$(ROOT_CA_CRT)" MFG_CRT="$(MFG_CRT)" MFG_KEY="$(MFG_KEY)" INTERMEDIATE_CA_CRT="$(INTERMEDIATE_CA_CRT)" INTERMEDIATE_CA_KEY=$(INTERMEDIATE_CA_KEY) go test -race -v ./bridge/... -coverpkg=./... -covermode=atomic -coverprofile=$(TMP_PATH)/bridge.unit.coverage.txt + ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(ROOT_CA_KEY)" \ + MFG_CRT="$(MFG_CRT)" MFG_KEY="$(MFG_KEY)" \ + INTERMEDIATE_CA_CRT="$(INTERMEDIATE_CA_CRT)" INTERMEDIATE_CA_KEY=$(INTERMEDIATE_CA_KEY) \ + COAP_CRT="$(COAP_CRT)" COAP_KEY="$(COAP_KEY)" \ + CLOUD_SID=$(CLOUD_SID) \ + go test -race -parallel 1 -v ./bridge/... -coverpkg=./... -covermode=atomic -coverprofile=$(TMP_PATH)/bridge.unit.coverage.txt go test -race -v ./schema/... -covermode=atomic -coverprofile=$(TMP_PATH)/schema.unit.coverage.txt - ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(CERT_PATH)/cloudcakey.pem" go test -race -v ./pkg/... -covermode=atomic -coverprofile=$(TMP_PATH)/pkg.unit.coverage.txt + ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(ROOT_CA_KEY)" \ + go test -race -v ./pkg/... -covermode=atomic -coverprofile=$(TMP_PATH)/pkg.unit.coverage.txt test: env build-testcontainer docker run \ diff --git a/bridge/device/cloud/manager.go b/bridge/device/cloud/manager.go index 34e6f304..930dec3e 100644 --- a/bridge/device/cloud/manager.go +++ b/bridge/device/cloud/manager.go @@ -31,6 +31,7 @@ import ( "github.com/plgd-dev/device/v2/bridge/resources" "github.com/plgd-dev/device/v2/bridge/resources/discovery" "github.com/plgd-dev/device/v2/pkg/codec/cbor" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/device/v2/schema/device" @@ -68,7 +69,7 @@ type Manager struct { cfg Configuration } - creds CoapSignUpResponse + creds ocfCloud.CoapSignUpResponse client *client.Conn signedIn bool resourcesPublished bool @@ -115,7 +116,7 @@ func (c *Manager) ImportConfig(cfg Config) { URL: cfg.CloudURL, CloudID: cfg.CloudID, }) - c.setCreds(CoapSignUpResponse{ + c.setCreds(ocfCloud.CoapSignUpResponse{ AccessToken: cfg.AccessToken, UserID: cfg.UserID, RefreshToken: cfg.RefreshToken, @@ -138,7 +139,7 @@ func (c *Manager) resetCredentials(ctx context.Context, signOff bool) { log.Printf("%v\n", err) } } - c.creds = CoapSignUpResponse{} + c.creds = ocfCloud.CoapSignUpResponse{} c.signedIn = false c.resourcesPublished = false if err := c.close(); err != nil { @@ -226,12 +227,12 @@ func validUntil(expiresIn int64) time.Time { return time.Now().Add(time.Duration(expiresIn) * time.Second) } -func (c *Manager) setCreds(creds CoapSignUpResponse) { +func (c *Manager) setCreds(creds ocfCloud.CoapSignUpResponse) { c.creds = creds c.signedIn = false } -func (c *Manager) getCreds() CoapSignUpResponse { +func (c *Manager) getCreds() ocfCloud.CoapSignUpResponse { return c.creds } diff --git a/bridge/device/cloud/manager_test.go b/bridge/device/cloud/manager_test.go new file mode 100644 index 00000000..d94e7ca1 --- /dev/null +++ b/bridge/device/cloud/manager_test.go @@ -0,0 +1,110 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package cloud_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/device/cloud" + bridgeTest "github.com/plgd-dev/device/v2/bridge/test" + cloudSchema "github.com/plgd-dev/device/v2/schema/cloud" + testClient "github.com/plgd-dev/device/v2/test/client" + mockCoapGW "github.com/plgd-dev/device/v2/test/coap-gateway" + mockCoapGWService "github.com/plgd-dev/device/v2/test/coap-gateway/service" + "github.com/stretchr/testify/require" +) + +// device is restarted with an imported configuration with valid cloud credentials +func TestProvisioningOnDeviceRestart(t *testing.T) { + ch := mockCoapGW.NewCoapHandlerWithCounter(-1) + makeHandler := func(s *mockCoapGWService.Service, opts ...mockCoapGWService.Option) mockCoapGWService.ServiceHandler { + return ch + } + coapShutdown := mockCoapGW.New(t, makeHandler, func(handler mockCoapGWService.ServiceHandler) { + h := handler.(*mockCoapGW.DefaultHandlerWithCounter) + fmt.Printf("%+v\n", h.CallCounter.Data) + // d1 -> signup + signin + publish + // d2 -> should use the stored credentials to skip signup and only do sign in + publish + require.Equal(t, 1, h.CallCounter.Data[mockCoapGW.SignUpKey]) + require.Equal(t, 2, h.CallCounter.Data[mockCoapGW.SignInKey]) + require.Equal(t, 2, h.CallCounter.Data[mockCoapGW.PublishKey]) + require.Equal(t, 0, h.CallCounter.Data[mockCoapGW.RefreshTokenKey]) + }) + defer coapShutdown() + + s1 := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s1.Shutdown() + }) + deviceID := uuid.New().String() + d1 := bridgeTest.NewBridgedDevice(t, s1, true, deviceID) + s1Shutdown := bridgeTest.RunBridgeService(s1) + t.Cleanup(func() { + _ = s1Shutdown() + }) + + c, err := testClient.NewTestSecureClientWithBridgeSupport() + require.NoError(t, err) + defer func() { + errC := c.Close(context.Background()) + require.NoError(t, errC) + }() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + err = c.OnboardDevice(ctx, deviceID, "authorizationProvider", "coaps+tcp://"+mockCoapGW.COAP_GW_HOST, "authorizationCode", "cloudID") + require.NoError(t, err) + + // wait for sign in + require.Equal(t, 1, ch.WaitForSignIn(time.Second*20)) + + // stop service + err = s1Shutdown() + require.NoError(t, err) + + // save the device configuration + cfg := d1.ExportConfig() + + // recreate device using the saved configuration from a signed in device + s2 := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s2.Shutdown() + }) + d2 := bridgeTest.NewBridgedDeviceWithConfig(t, s2, cfg) + s2Shutdown := bridgeTest.RunBridgeService(s2) + defer func() { + errS := s2Shutdown() + require.NoError(t, errS) + }() + require.Equal(t, 2, ch.WaitForSignIn(time.Second*20)) + + // check provisioning status + var cloudCfg cloud.Configuration + err = c.GetResource(ctx, deviceID, cloudSchema.ResourceURI, &cloudCfg) + require.NoError(t, err) + require.Equal(t, cloudCfg.ProvisioningStatus, cloudSchema.ProvisioningStatus_REGISTERED) + + // sign off + d2.UnregisterFromCloud() + require.Equal(t, 1, ch.WaitForSignOff(time.Second*20)) +} diff --git a/bridge/device/cloud/publishResources.go b/bridge/device/cloud/publishResources.go index 2136f52b..070bd732 100644 --- a/bridge/device/cloud/publishResources.go +++ b/bridge/device/cloud/publishResources.go @@ -24,16 +24,11 @@ import ( "log" "github.com/plgd-dev/device/v2/bridge/resources" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/go-coap/v3/message/codes" ) -type PublishResourcesRequest struct { - DeviceID string `json:"di"` - Links schema.ResourceLinks `json:"links"` - TimeToLive int `json:"ttl"` -} - var ErrCannotPublishResources = fmt.Errorf("cannot publish resources") func errCannotPublishResources(err error) error { @@ -47,12 +42,12 @@ func (c *Manager) publishResources(ctx context.Context) error { links := c.getLinks(schema.Endpoints{}, c.deviceID, nil, resources.PublishToCloud) links = patchDeviceLink(links) - wkRd := PublishResourcesRequest{ + wkRd := ocfCloud.PublishResourcesRequest{ DeviceID: c.deviceID.String(), Links: links, TimeToLive: 0, } - req, err := newPostRequest(ctx, c.client, ResourceDirectory, wkRd) + req, err := newPostRequest(ctx, c.client, ocfCloud.ResourceDirectory, wkRd) if err != nil { return errCannotPublishResources(err) } @@ -64,6 +59,6 @@ func (c *Manager) publishResources(ctx context.Context) error { return errCannotPublishResources(fmt.Errorf("unexpected status code %v", resp.Code())) } c.resourcesPublished = true - log.Printf("resourcesPublished\n") + log.Printf("resources published\n") return nil } diff --git a/bridge/device/cloud/refreshToken.go b/bridge/device/cloud/refreshToken.go index 5e6589be..a298d91f 100644 --- a/bridge/device/cloud/refreshToken.go +++ b/bridge/device/cloud/refreshToken.go @@ -24,22 +24,11 @@ import ( "log" "github.com/plgd-dev/device/v2/pkg/codec/cbor" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/go-coap/v3/message/codes" ) -type CoapRefreshTokenRequest struct { - DeviceID string `json:"di"` - UserID string `json:"uid"` - RefreshToken string `json:"refreshtoken"` -} - -type CoapRefreshTokenResponse struct { - AccessToken string `json:"accesstoken"` - RefreshToken string `json:"refreshtoken"` - ExpiresIn int64 `json:"expiresin"` -} - var ErrCannotRefreshToken = fmt.Errorf("cannot refresh token") func errCannotRefreshToken(err error) error { @@ -51,7 +40,7 @@ func (c *Manager) refreshToken(ctx context.Context) error { if creds.RefreshToken == "" { return nil } - req, err := newPostRequest(ctx, c.client, RefreshToken, CoapRefreshTokenRequest{ + req, err := newPostRequest(ctx, c.client, ocfCloud.RefreshToken, ocfCloud.CoapRefreshTokenRequest{ DeviceID: c.deviceID.String(), UserID: creds.UserID, RefreshToken: creds.RefreshToken, @@ -70,17 +59,17 @@ func (c *Manager) refreshToken(ctx context.Context) error { } return errCannotRefreshToken(fmt.Errorf("unexpected status code %v", resp.Code())) } - var refreshResp CoapRefreshTokenResponse + var refreshResp ocfCloud.CoapRefreshTokenResponse if err = cbor.ReadFrom(resp.Body(), &refreshResp); err != nil { return errCannotRefreshToken(err) } c.updateCredsByRefreshTokenResponse(refreshResp) - log.Printf("refresh token\n") + log.Printf("refreshed token\n") c.save() return nil } -func (c *Manager) updateCredsByRefreshTokenResponse(resp CoapRefreshTokenResponse) { +func (c *Manager) updateCredsByRefreshTokenResponse(resp ocfCloud.CoapRefreshTokenResponse) { c.creds.AccessToken = resp.AccessToken c.creds.RefreshToken = resp.RefreshToken c.creds.ValidUntil = validUntil(resp.ExpiresIn) diff --git a/bridge/device/cloud/signIn.go b/bridge/device/cloud/signIn.go index 31afeb83..0e44fbf9 100644 --- a/bridge/device/cloud/signIn.go +++ b/bridge/device/cloud/signIn.go @@ -24,31 +24,21 @@ import ( "log" "github.com/plgd-dev/device/v2/pkg/codec/cbor" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/go-coap/v3/message/codes" ) -type CoapSignInRequest struct { - DeviceID string `json:"di"` - UserID string `json:"uid"` - AccessToken string `json:"accesstoken"` - Login bool `json:"login"` -} - -type CoapSignInResponse struct { - ExpiresIn int64 `json:"expiresin"` -} - var ( ErrMissingAccessToken = fmt.Errorf("access token missing") ErrCannotSignIn = fmt.Errorf("cannot sign in") ) -func MakeSignInRequest(deviceID, userID, accessToken string) (CoapSignInRequest, error) { +func MakeSignInRequest(deviceID, userID, accessToken string) (ocfCloud.CoapSignInRequest, error) { if accessToken == "" { - return CoapSignInRequest{}, ErrMissingAccessToken + return ocfCloud.CoapSignInRequest{}, ErrMissingAccessToken } - return CoapSignInRequest{ + return ocfCloud.CoapSignInRequest{ DeviceID: deviceID, UserID: userID, AccessToken: accessToken, @@ -72,7 +62,7 @@ func (c *Manager) signIn(ctx context.Context) error { if err != nil { return errCannotSignIn(err) } - req, err := newPostRequest(ctx, c.client, SignIn, signInReq) + req, err := newPostRequest(ctx, c.client, ocfCloud.SignIn, signInReq) if err != nil { return errCannotSignIn(err) } @@ -87,7 +77,7 @@ func (c *Manager) signIn(ctx context.Context) error { } return errCannotSignIn(fmt.Errorf("unexpected status code %v", resp.Code())) } - var signInResp CoapSignInResponse + var signInResp ocfCloud.CoapSignInResponse err = cbor.ReadFrom(resp.Body(), &signInResp) if err != nil { return err @@ -98,7 +88,7 @@ func (c *Manager) signIn(ctx context.Context) error { return nil } -func (c *Manager) updateCredsBySignInResponse(resp CoapSignInResponse) { +func (c *Manager) updateCredsBySignInResponse(resp ocfCloud.CoapSignInResponse) { c.creds.ExpiresIn = resp.ExpiresIn c.creds.ValidUntil = validUntil(resp.ExpiresIn) c.signedIn = true diff --git a/bridge/device/cloud/signOff.go b/bridge/device/cloud/signOff.go index a44e6e47..d9488fc8 100644 --- a/bridge/device/cloud/signOff.go +++ b/bridge/device/cloud/signOff.go @@ -23,6 +23,7 @@ import ( "fmt" "log" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/message/pool" @@ -34,7 +35,7 @@ const ProvisioningStatusDEREGISTERING cloud.ProvisioningStatus = "deregistering" var ErrCannotSignOff = fmt.Errorf("cannot sign off") func newSignOffReq(ctx context.Context, c *client.Conn, deviceID, userID string) (*pool.Message, error) { - req, err := newRequestWithToken(ctx, c, SignUp) + req, err := newRequestWithToken(ctx, c, ocfCloud.SignUp) if err != nil { return nil, err } diff --git a/bridge/device/cloud/signUp.go b/bridge/device/cloud/signUp.go index 998f5a1e..7171b887 100644 --- a/bridge/device/cloud/signUp.go +++ b/bridge/device/cloud/signUp.go @@ -22,43 +22,28 @@ import ( "context" "fmt" "log" - "time" "github.com/plgd-dev/device/v2/pkg/codec/cbor" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/go-coap/v3/message/codes" ) -type CoapSignUpRequest struct { - DeviceID string `json:"di"` - AuthorizationCode string `json:"accesstoken"` - AuthorizationProvider string `json:"authprovider"` -} - -type CoapSignUpResponse struct { - AccessToken string `yaml:"accessToken" json:"accesstoken"` - UserID string `yaml:"userID" json:"uid"` - RefreshToken string `yaml:"refreshToken" json:"refreshtoken"` - RedirectURI string `yaml:"-" json:"redirecturi"` - ExpiresIn int64 `yaml:"-" json:"expiresin"` - ValidUntil time.Time `yaml:"-" jsom:"-"` -} - var ( ErrMissingAuthorizationCode = fmt.Errorf("authorization code missing") ErrMissingAuthorizationProvider = fmt.Errorf("authorization provider missing") ErrCannotSignUp = fmt.Errorf("cannot sign up") ) -func MakeSignUpRequest(deviceID, code, provider string) (CoapSignUpRequest, error) { +func MakeSignUpRequest(deviceID, code, provider string) (ocfCloud.CoapSignUpRequest, error) { if code == "" { - return CoapSignUpRequest{}, ErrMissingAuthorizationCode + return ocfCloud.CoapSignUpRequest{}, ErrMissingAuthorizationCode } if provider == "" { - return CoapSignUpRequest{}, ErrMissingAuthorizationProvider + return ocfCloud.CoapSignUpRequest{}, ErrMissingAuthorizationProvider } - return CoapSignUpRequest{ + return ocfCloud.CoapSignUpRequest{ DeviceID: deviceID, AuthorizationCode: code, AuthorizationProvider: provider, @@ -79,7 +64,7 @@ func (c *Manager) signUp(ctx context.Context) error { if err != nil { return errCannotSignUp(err) } - req, err := newPostRequest(ctx, c.client, SignUp, signUpRequest) + req, err := newPostRequest(ctx, c.client, ocfCloud.SignUp, signUpRequest) if err != nil { return errCannotSignUp(err) } @@ -91,7 +76,7 @@ func (c *Manager) signUp(ctx context.Context) error { if resp.Code() != codes.Changed { return errCannotSignUp(fmt.Errorf("unexpected status code %v", resp.Code())) } - var signUpResp CoapSignUpResponse + var signUpResp ocfCloud.CoapSignUpResponse err = cbor.ReadFrom(resp.Body(), &signUpResp) if err != nil { return errCannotSignUp(err) diff --git a/bridge/device/config.go b/bridge/device/config.go index 578cae9a..8d63bfef 100644 --- a/bridge/device/config.go +++ b/bridge/device/config.go @@ -50,5 +50,6 @@ func (cfg *Config) Validate() error { if cfg.Name == "" { cfg.Name = "Unnamed" } + return nil } diff --git a/bridge/getDevices_test.go b/bridge/getDevices_test.go index df751514..c67e0ce8 100644 --- a/bridge/getDevices_test.go +++ b/bridge/getDevices_test.go @@ -31,6 +31,9 @@ import ( func TestGetDevices(t *testing.T) { s := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s.Shutdown() + }) deviceID1 := uuid.New().String() d1 := bridgeTest.NewBridgedDevice(t, s, false, deviceID1) deviceID2 := uuid.New().String() @@ -44,7 +47,10 @@ func TestGetDevices(t *testing.T) { }() cleanup := bridgeTest.RunBridgeService(s) - defer cleanup() + defer func() { + errC := cleanup() + require.NoError(t, errC) + }() c, err := testClient.NewTestSecureClientWithBridgeSupport() require.NoError(t, err) diff --git a/bridge/getResource_test.go b/bridge/getResource_test.go index ef725162..c346c97c 100644 --- a/bridge/getResource_test.go +++ b/bridge/getResource_test.go @@ -38,6 +38,9 @@ import ( func TestGetResource(t *testing.T) { s := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s.Shutdown() + }) deviceID1 := uuid.New().String() d1 := bridgeTest.NewBridgedDevice(t, s, false, deviceID1) deviceID2 := uuid.New().String() @@ -56,7 +59,10 @@ func TestGetResource(t *testing.T) { d1.AddResource(failRes) cleanup := bridgeTest.RunBridgeService(s) - defer cleanup() + defer func() { + errC := cleanup() + require.NoError(t, errC) + }() type args struct { deviceID string diff --git a/bridge/observeResource_test.go b/bridge/observeResource_test.go index 4cfbae80..42cd2a2d 100644 --- a/bridge/observeResource_test.go +++ b/bridge/observeResource_test.go @@ -24,6 +24,9 @@ import ( func TestObserveResource(t *testing.T) { s := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s.Shutdown() + }) d := bridgeTest.NewBridgedDevice(t, s, false, uuid.New().String()) defer func() { s.DeleteAndCloseDevice(d.GetID()) @@ -87,7 +90,10 @@ func TestObserveResource(t *testing.T) { d.AddResource(res) cleanup := bridgeTest.RunBridgeService(s) - defer cleanup() + defer func() { + errC := cleanup() + require.NoError(t, errC) + }() c, err := testClient.NewTestSecureClientWithBridgeSupport() require.NoError(t, err) diff --git a/bridge/onboardDevice_test.go b/bridge/onboardDevice_test.go index bdd4ba0e..a7fa0f21 100644 --- a/bridge/onboardDevice_test.go +++ b/bridge/onboardDevice_test.go @@ -33,13 +33,19 @@ import ( func TestOnboardDevice(t *testing.T) { s := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s.Shutdown() + }) deviceID := uuid.New().String() d := bridgeTest.NewBridgedDevice(t, s, true, deviceID) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() cleanup := bridgeTest.RunBridgeService(s) - defer cleanup() + defer func() { + errC := cleanup() + require.NoError(t, errC) + }() type args struct { deviceID string diff --git a/bridge/test/test.go b/bridge/test/test.go index 6640e8ce..5131dac0 100644 --- a/bridge/test/test.go +++ b/bridge/test/test.go @@ -47,9 +47,9 @@ func NewBridgeService(t *testing.T) *service.Service { return s } -func RunBridgeService(s *service.Service) func() { - cleanup := func() { - _ = s.Shutdown() +func RunBridgeService(s *service.Service) func() error { + cleanup := func() error { + return s.Shutdown() } go func() { _ = s.Serve() @@ -57,18 +57,36 @@ func RunBridgeService(s *service.Service) func() { return cleanup } +func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Config) service.Device { + newDevice := func(di uuid.UUID, piid uuid.UUID) service.Device { + cfg.ID = di + cfg.ProtocolIndependentID = piid + require.NoError(t, cfg.Validate()) + return device.New(cfg, nil, nil) + } + d, ok := s.CreateDevice(cfg.ID, newDevice) + require.True(t, ok) + d.Init() + return d +} + +func makeDeviceConfig(id uuid.UUID, cloudEnabled bool) device.Config { + cfg := device.Config{ + ID: id, + Name: "bridged-device", + ResourceTypes: []string{"oic.d.virtual"}, + MaxMessageSize: 1024 * 256, + } + if cloudEnabled { + cfg.Cloud.Enabled = true + } + return cfg +} + func NewBridgedDevice(t *testing.T, s *service.Service, cloudEnabled bool, id string) service.Device { - newDevice := func(id uuid.UUID, piid uuid.UUID) service.Device { - cfg := device.Config{ - Name: "bridged-device", - ResourceTypes: []string{"oic.d.virtual"}, - ID: id, - ProtocolIndependentID: piid, - MaxMessageSize: 1024 * 256, - } - if cloudEnabled { - cfg.Cloud.Enabled = true - } + newDevice := func(di uuid.UUID, piid uuid.UUID) service.Device { + cfg := makeDeviceConfig(di, cloudEnabled) + cfg.ProtocolIndependentID = piid require.NoError(t, cfg.Validate()) return device.New(cfg, nil, nil) } diff --git a/bridge/updateResource_test.go b/bridge/updateResource_test.go index ffc6ef64..904c2844 100644 --- a/bridge/updateResource_test.go +++ b/bridge/updateResource_test.go @@ -54,6 +54,9 @@ func (r *resourceDataSync) copy() resourceData { func TestUpdateResource(t *testing.T) { s := bridgeTest.NewBridgeService(t) + t.Cleanup(func() { + _ = s.Shutdown() + }) d := bridgeTest.NewBridgedDevice(t, s, false, uuid.New().String()) defer func() { s.DeleteAndCloseDevice(d.GetID()) @@ -90,7 +93,10 @@ func TestUpdateResource(t *testing.T) { d.AddResource(res) cleanup := bridgeTest.RunBridgeService(s) - defer cleanup() + defer func() { + errC := cleanup() + require.NoError(t, errC) + }() c, err := testClient.NewTestSecureClient() require.NoError(t, err) diff --git a/pkg/ocf/cloud/request.go b/pkg/ocf/cloud/request.go new file mode 100644 index 00000000..c72885b0 --- /dev/null +++ b/pkg/ocf/cloud/request.go @@ -0,0 +1,74 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package cloud + +import ( + "time" + + "github.com/plgd-dev/device/v2/schema" +) + +type CoapSignUpRequest struct { + DeviceID string `json:"di"` + AuthorizationCode string `json:"accesstoken"` + AuthorizationProvider string `json:"authprovider"` +} + +type CoapSignUpResponse struct { + AccessToken string `yaml:"accessToken" json:"accesstoken"` + UserID string `yaml:"userID" json:"uid"` + RefreshToken string `yaml:"refreshToken" json:"refreshtoken"` + RedirectURI string `yaml:"-" json:"redirecturi"` + ExpiresIn int64 `yaml:"-" json:"expiresin"` + ValidUntil time.Time `yaml:"-" jsom:"-"` +} + +type CoapSignInRequest struct { + DeviceID string `json:"di"` + UserID string `json:"uid"` + AccessToken string `json:"accesstoken"` + Login bool `json:"login"` +} + +type CoapSignInResponse struct { + ExpiresIn int64 `json:"expiresin"` +} + +type CoapRefreshTokenRequest struct { + DeviceID string `json:"di"` + UserID string `json:"uid"` + RefreshToken string `json:"refreshtoken"` +} + +type CoapRefreshTokenResponse struct { + AccessToken string `json:"accesstoken"` + RefreshToken string `json:"refreshtoken"` + ExpiresIn int64 `json:"expiresin"` +} + +type PublishResourcesRequest struct { + DeviceID string `json:"di"` + Links schema.ResourceLinks `json:"links"` + TimeToLive int `json:"ttl"` +} + +type UnpublishResourcesRequest struct { + DeviceID string + InstanceIDs []int64 +} diff --git a/bridge/device/cloud/uri.go b/pkg/ocf/cloud/uri.go similarity index 96% rename from bridge/device/cloud/uri.go rename to pkg/ocf/cloud/uri.go index ccad807c..e042f5da 100644 --- a/bridge/device/cloud/uri.go +++ b/pkg/ocf/cloud/uri.go @@ -1,6 +1,6 @@ /**************************************************************************** * - * Copyright (c) 2023 plgd.dev s.r.o. + * Copyright (c) 2024 plgd.dev s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. diff --git a/sonar-project.properties b/sonar-project.properties index c1fc71e1..4b4f4c5d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -17,5 +17,9 @@ sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.test.exclusions= +# excludes duplications check +sonar.cpd.exclusions=cmd/**/*.go + +# exludes from code coverage report sonar.go.coverage.reportPaths=coverage/**/*coverage*.txt sonar.coverage.exclusions=bridge/test/**/*.go,test/**/*.go,client/app/app.go,cmd/ocfclient/ocfclient.go,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py diff --git a/test/coap-gateway/defaultHandler.go b/test/coap-gateway/defaultHandler.go new file mode 100644 index 00000000..af61bcf8 --- /dev/null +++ b/test/coap-gateway/defaultHandler.go @@ -0,0 +1,240 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package test + +import ( + "fmt" + "sync" + "time" + + "github.com/plgd-dev/device/v2/pkg/ocf/cloud" +) + +// DefaultHandler is the default handler for tests +// +// It implements ServiceHandler interface by just logging the called method and +// returning default response and no error (if required). +type DefaultHandler struct { + deviceID string + accessToken string + refreshToken string + accessTokenLifetime int64 // lifetime in seconds or <0 value for a token without expiration +} + +func MakeDefaultHandler(accessTokenLifetime int64) DefaultHandler { + return DefaultHandler{ + accessToken: "access-token", + refreshToken: "refresh-token", + accessTokenLifetime: accessTokenLifetime, + } +} + +func (h *DefaultHandler) GetDeviceID() string { + return h.deviceID +} + +func (h *DefaultHandler) SetDeviceID(deviceID string) { + h.deviceID = deviceID +} + +func (h *DefaultHandler) SetAccessToken(accessToken string) { + h.accessToken = accessToken +} + +func (h *DefaultHandler) SetRefreshToken(refreshToken string) { + h.refreshToken = refreshToken +} + +func (h *DefaultHandler) SignUp(req cloud.CoapSignUpRequest) (cloud.CoapSignUpResponse, error) { + fmt.Printf("SignUp: %v\n", req) + h.SetDeviceID(req.DeviceID) + return cloud.CoapSignUpResponse{ + AccessToken: h.accessToken, + UserID: "1", + RefreshToken: h.refreshToken, + ExpiresIn: h.accessTokenLifetime, + RedirectURI: "", + }, nil +} + +func (h *DefaultHandler) CloseOnError() bool { + return true +} + +func (h *DefaultHandler) SignOff() error { + fmt.Printf("SignOff deviceID:%v\n", h.deviceID) + return nil +} + +func (h *DefaultHandler) SignIn(req cloud.CoapSignInRequest) (cloud.CoapSignInResponse, error) { + fmt.Printf("SignIn: %v\n", req) + return cloud.CoapSignInResponse{ + ExpiresIn: h.accessTokenLifetime, + }, nil +} + +func (h *DefaultHandler) SignOut(req cloud.CoapSignInRequest) error { + fmt.Printf("SignOut: %v\n", req) + return nil +} + +func (h *DefaultHandler) PublishResources(req cloud.PublishResourcesRequest) error { + fmt.Printf("PublishResources: %v\n", req) + return nil +} + +func (h *DefaultHandler) UnpublishResources(req cloud.UnpublishResourcesRequest) error { + fmt.Printf("UnpublishResources: %v\n", req) + return nil +} + +func (h *DefaultHandler) RefreshToken(req cloud.CoapRefreshTokenRequest) (cloud.CoapRefreshTokenResponse, error) { + fmt.Printf("RefreshToken: %v\n", req) + return cloud.CoapRefreshTokenResponse{ + RefreshToken: h.refreshToken, + AccessToken: h.accessToken, + ExpiresIn: h.accessTokenLifetime, + }, nil +} + +const ( + SignUpKey = "SignUp" // register + SignOffKey = "SignOff" // deregister + SignInKey = "SignIn" // log in + SignOutKey = "SignOut" // log out + PublishKey = "Publish" + UnpublishKey = "Unpublish" + RefreshTokenKey = "RefreshToken" +) + +type DefaultHandlerWithCounter struct { + *DefaultHandler + + CallCounter struct { + Data map[string]int + Lock sync.Mutex + } + + signedInChan chan int + signedOffChan chan int +} + +func NewCoapHandlerWithCounter(atLifetime int64) *DefaultHandlerWithCounter { + dh := MakeDefaultHandler(atLifetime) + return &DefaultHandlerWithCounter{ + DefaultHandler: &dh, + CallCounter: struct { + Data map[string]int + Lock sync.Mutex + }{ + Data: make(map[string]int), + }, + + signedInChan: make(chan int, 16), + signedOffChan: make(chan int, 16), + } +} + +func sendToChan(c chan int, v int) { + select { + case c <- v: + default: + } +} + +func waitForAction(c chan int, timeout time.Duration) int { + select { + case v := <-c: + return v + case <-time.After(timeout): + return -1 + } +} + +func (ch *DefaultHandlerWithCounter) SignUp(req cloud.CoapSignUpRequest) (cloud.CoapSignUpResponse, error) { + resp, err := ch.DefaultHandler.SignUp(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[SignUpKey]++ + ch.CallCounter.Lock.Unlock() + return resp, err +} + +func (ch *DefaultHandlerWithCounter) SignOff() error { + err := ch.DefaultHandler.SignOff() + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[SignOffKey]++ + signOffCount, ok := ch.CallCounter.Data[SignOffKey] + if ok { + sendToChan(ch.signedOffChan, signOffCount) + } + ch.CallCounter.Lock.Unlock() + return err +} + +func (ch *DefaultHandlerWithCounter) WaitForSignOff(timeout time.Duration) int { + return waitForAction(ch.signedOffChan, timeout) +} + +func (ch *DefaultHandlerWithCounter) SignIn(req cloud.CoapSignInRequest) (cloud.CoapSignInResponse, error) { + resp, err := ch.DefaultHandler.SignIn(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[SignInKey]++ + signInCount, ok := ch.CallCounter.Data[SignInKey] + if ok { + sendToChan(ch.signedInChan, signInCount) + } + ch.CallCounter.Lock.Unlock() + return resp, err +} + +func (ch *DefaultHandlerWithCounter) WaitForSignIn(timeout time.Duration) int { + return waitForAction(ch.signedInChan, timeout) +} + +func (ch *DefaultHandlerWithCounter) SignOut(req cloud.CoapSignInRequest) error { + err := ch.DefaultHandler.SignOut(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[SignOutKey]++ + ch.CallCounter.Lock.Unlock() + return err +} + +func (ch *DefaultHandlerWithCounter) PublishResources(req cloud.PublishResourcesRequest) error { + err := ch.DefaultHandler.PublishResources(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[PublishKey]++ + ch.CallCounter.Lock.Unlock() + return err +} + +func (ch *DefaultHandlerWithCounter) UnpublishResources(req cloud.UnpublishResourcesRequest) error { + err := ch.DefaultHandler.UnpublishResources(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[UnpublishKey]++ + ch.CallCounter.Lock.Unlock() + return err +} + +func (ch *DefaultHandlerWithCounter) RefreshToken(req cloud.CoapRefreshTokenRequest) (cloud.CoapRefreshTokenResponse, error) { + resp, err := ch.DefaultHandler.RefreshToken(req) + ch.CallCounter.Lock.Lock() + ch.CallCounter.Data[RefreshTokenKey]++ + ch.CallCounter.Lock.Unlock() + return resp, err +} diff --git a/test/coap-gateway/service/client.go b/test/coap-gateway/service/client.go new file mode 100644 index 00000000..5c83f73f --- /dev/null +++ b/test/coap-gateway/service/client.go @@ -0,0 +1,129 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + coapTcpClient "github.com/plgd-dev/go-coap/v3/tcp/client" + grpcCodes "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Client a setup of connection +type Client struct { + server *Service + coapConn *coapTcpClient.Conn + handler ServiceHandler + + deviceID string +} + +// newClient creates and initializes client +func newClient(server *Service, client *coapTcpClient.Conn, handler ServiceHandler) *Client { + return &Client{ + server: server, + coapConn: client, + handler: handler, + } +} + +func (c *Client) GetCoapConnection() *coapTcpClient.Conn { + return c.coapConn +} + +func (c *Client) GetServiceHandler() ServiceHandler { + return c.handler +} + +func (c *Client) GetDeviceID() string { + return c.deviceID +} + +func (c *Client) SetDeviceID(deviceID string) { + c.deviceID = deviceID +} + +func (c *Client) RemoteAddrString() string { + return c.coapConn.RemoteAddr().String() +} + +func (c *Client) Context() context.Context { + return c.coapConn.Context() +} + +// Close closes coap connection +func (c *Client) Close() error { + if err := c.coapConn.Close(); err != nil { + return fmt.Errorf("cannot close client: %w", err) + } + return nil +} + +// OnClose is invoked when the coap connection was closed. +func (c *Client) OnClose() { + fmt.Printf("close client %v\n", c.coapConn.RemoteAddr()) +} + +type grpcErr interface { + GRPCStatus() *status.Status +} + +func isContextCanceled(err error) bool { + if errors.Is(err, context.Canceled) { + return true + } + var gErr grpcErr + if ok := errors.As(err, &gErr); ok { + return gErr.GRPCStatus().Code() == grpcCodes.Canceled + } + return false +} + +func (c *Client) sendResponse(code codes.Code, token message.Token, payload []byte) { + msg := pool.NewMessage(c.Context()) + msg.SetCode(code) + msg.SetToken(token) + if len(payload) > 0 { + msg.SetContentFormat(message.AppOcfCbor) + msg.SetBody(bytes.NewReader(payload)) + } + if err := c.coapConn.WriteMessage(msg); err != nil { + if !isContextCanceled(err) { + fmt.Printf("cannot send reply to %v: %v\n", c.GetDeviceID(), err) + } + } +} + +func (c *Client) sendErrorResponse(err error, code codes.Code, token message.Token) { + msg := pool.NewMessage(c.Context()) + msg.SetCode(code) + msg.SetToken(token) + // Don't set content format for diagnostic message: https://tools.ietf.org/html/rfc7252#section-5.5.2 + msg.SetBody(bytes.NewReader([]byte(err.Error()))) + if err = c.coapConn.WriteMessage(msg); err != nil { + fmt.Printf("cannot send error to %v: %v\n", c.GetDeviceID(), err) + } +} diff --git a/test/coap-gateway/service/config.go b/test/coap-gateway/service/config.go new file mode 100644 index 00000000..602b75c9 --- /dev/null +++ b/test/coap-gateway/service/config.go @@ -0,0 +1,31 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import "crypto/tls" + +type TLSConfig struct { + Enabled bool + *tls.Config +} + +type Config struct { + Addr string + TLS TLSConfig +} diff --git a/test/coap-gateway/service/refreshToken.go b/test/coap-gateway/service/refreshToken.go new file mode 100644 index 00000000..18d054f5 --- /dev/null +++ b/test/coap-gateway/service/refreshToken.go @@ -0,0 +1,52 @@ +package service + +import ( + "fmt" + + "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/kit/v2/codec/cbor" +) + +func refreshTokenPostHandler(req *mux.Message, client *Client) { + logErrorAndCloseClient := func(err error, code coapCodes.Code) { + client.sendErrorResponse(fmt.Errorf("cannot handle refresh token: %w", err), code, req.Token()) + if client.handler == nil || client.handler.CloseOnError() { + if err := client.Close(); err != nil { + fmt.Printf("refresh token error: %v\n", err) + } + } + } + + var r cloud.CoapRefreshTokenRequest + err := cbor.ReadFrom(req.Body(), &r) + if err != nil { + logErrorAndCloseClient(err, coapCodes.BadRequest) + return + } + + resp, err := client.handler.RefreshToken(r) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + out, err := cbor.Encode(resp) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + client.sendResponse(coapCodes.Changed, req.Token(), out) +} + +// RefreshToken +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.tokenrefresh.swagger.json +func refreshTokenHandler(req *mux.Message, client *Client) { + if req.Code() == coapCodes.POST { + refreshTokenPostHandler(req, client) + return + } + client.sendErrorResponse(fmt.Errorf("forbidden request from %v", client.RemoteAddrString()), coapCodes.Forbidden, req.Token()) +} diff --git a/test/coap-gateway/service/resourceDirectory.go b/test/coap-gateway/service/resourceDirectory.go new file mode 100644 index 00000000..0e668c67 --- /dev/null +++ b/test/coap-gateway/service/resourceDirectory.go @@ -0,0 +1,131 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/kit/v2/codec/cbor" +) + +// fixHref ensures that href starts with "/" and does not end with "/". +func fixHref(href string) string { + backslash := regexp.MustCompile(`\/+`) + p := backslash.ReplaceAllString(href, "/") + p = strings.TrimRight(p, "/") + if len(p) > 0 && p[0] == '/' { + return p + } + return "/" + p +} + +func publishHandler(req *mux.Message, client *Client) { + p := cloud.PublishResourcesRequest{ + TimeToLive: -1, + } + err := cbor.ReadFrom(req.Body(), &p) + if err != nil { + client.sendErrorResponse(fmt.Errorf("cannot read publish request body received: %w", err), coapCodes.BadRequest, req.Token()) + return + } + + for i, link := range p.Links { + p.Links[i].DeviceID = p.DeviceID + p.Links[i].Href = fixHref(link.Href) + } + + if err = client.handler.PublishResources(p); err != nil { + client.sendErrorResponse(err, coapCodes.InternalServerError, req.Token()) + return + } + + out, err := cbor.Encode(p) + if err != nil { + client.sendErrorResponse(err, coapCodes.InternalServerError, req.Token()) + return + } + + client.sendResponse(coapCodes.Changed, req.Token(), out) +} + +func parseUnpublishRequestFromQuery(queries []string) (cloud.UnpublishResourcesRequest, error) { + req := cloud.UnpublishResourcesRequest{} + for _, q := range queries { + values, err := url.ParseQuery(q) + if err != nil { + return cloud.UnpublishResourcesRequest{}, fmt.Errorf("cannot parse unpublish query: %w", err) + } + if di := values.Get("di"); di != "" { + req.DeviceID = di + } + + if ins := values.Get("ins"); ins != "" { + i, err := strconv.Atoi(ins) + if err != nil { + return cloud.UnpublishResourcesRequest{}, fmt.Errorf("cannot convert %v to number", ins) + } + req.InstanceIDs = append(req.InstanceIDs, int64(i)) + } + } + + if req.DeviceID == "" { + return cloud.UnpublishResourcesRequest{}, fmt.Errorf("deviceID not found") + } + return req, nil +} + +func unpublishHandler(req *mux.Message, client *Client) { + queries, err := req.Options().Queries() + if err != nil { + client.sendErrorResponse(fmt.Errorf("cannot query string from unpublish request from device %v: %w", client.GetDeviceID(), err), coapCodes.BadRequest, req.Token()) + return + } + + r, err := parseUnpublishRequestFromQuery(queries) + if err != nil { + client.sendErrorResponse(fmt.Errorf("unable to parse unpublish request query string from device %v: %w", client.GetDeviceID(), err), coapCodes.BadRequest, req.Token()) + return + } + + err = client.handler.UnpublishResources(r) + if err != nil { + client.sendErrorResponse(err, coapCodes.InternalServerError, req.Token()) + return + } + + client.sendResponse(coapCodes.Deleted, req.Token(), nil) +} + +func resourceDirectoryHandler(req *mux.Message, client *Client) { + switch req.Code() { + case coapCodes.POST: + publishHandler(req, client) + case coapCodes.DELETE: + unpublishHandler(req, client) + default: + client.sendErrorResponse(fmt.Errorf("forbidden request from %v", client.RemoteAddrString()), coapCodes.Forbidden, req.Token()) + } +} diff --git a/test/coap-gateway/service/service.go b/test/coap-gateway/service/service.go new file mode 100644 index 00000000..8473fbc4 --- /dev/null +++ b/test/coap-gateway/service/service.go @@ -0,0 +1,238 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + + bridgeNet "github.com/plgd-dev/device/v2/bridge/net" + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/go-coap/v3/net" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + coapTcpClient "github.com/plgd-dev/go-coap/v3/tcp/client" + coapTcpServer "github.com/plgd-dev/go-coap/v3/tcp/server" +) + +// Service is a configuration of coap-gateway +type Service struct { + coapServer *coapTcpServer.Server + listener coapTcpServer.Listener + closeFn []func() + ctx context.Context + cancel context.CancelFunc + sigs chan os.Signal + getHandler GetServiceHandler + clients []*Client +} + +func newListener(cfg Config) (coapTcpServer.Listener, func(), error) { + if !cfg.TLS.Enabled { + listener, err := net.NewTCPListener("tcp", cfg.Addr) + if err != nil { + return nil, nil, fmt.Errorf("cannot create tcp listener: %w", err) + } + closeListener := func() { + if errC := listener.Close(); errC != nil { + fmt.Printf("failed to close tcp listener: %v\n", errC) + } + } + return listener, closeListener, nil + } + + listener, err := net.NewTLSListener("tcp", cfg.Addr, cfg.TLS.Config) + if err != nil { + return nil, nil, fmt.Errorf("cannot create tcp-tls listener: %w", err) + } + closeFn := (func() { + if errC := listener.Close(); errC != nil { + fmt.Printf("failed to close tcp-tls listener: %v\n", errC) + } + }) + return listener, closeFn, nil +} + +// New creates server. +func New(ctx context.Context, cfg Config, getHandler GetServiceHandler) (*Service, error) { + var closeFn []func() + listener, closeListener, err := newListener(cfg) + if err != nil { + return nil, fmt.Errorf("cannot create listener: %w", err) + } + closeFn = append(closeFn, closeListener) + + ctx, cancel := context.WithCancel(ctx) + + s := Service{ + listener: listener, + closeFn: closeFn, + ctx: ctx, + cancel: cancel, + sigs: make(chan os.Signal, 1), + getHandler: getHandler, + clients: nil, + } + + if err := s.setupCoapServer(); err != nil { + return nil, fmt.Errorf("cannot setup coap server: %w", err) + } + + return &s, nil +} + +const clientKey = "client" + +func (s *Service) coapConnOnNew(coapConn *coapTcpClient.Conn) { + client := newClient(s, coapConn, s.getHandler(s, WithCoapConnectionOpt(coapConn))) + coapConn.SetContextValue(clientKey, client) + coapConn.AddOnClose(func() { + client.OnClose() + }) + s.clients = append(s.clients, client) +} + +func validateCommand(writer mux.ResponseWriter, request *mux.Message, server *Service, fnc func(req *mux.Message, client *Client)) { + request.Hijack() + go func(w mux.ResponseWriter, req *mux.Message) { + client, ok := w.Conn().Context().Value(clientKey).(*Client) + if !ok || client == nil { + con, ok2 := w.Conn().(*coapTcpClient.Conn) + if !ok2 { + panic("invalid connection") + } + client = newClient(server, con, nil) + } + closeClient := func(c *Client) { + if err := c.Close(); err != nil { + fmt.Printf("cannot handle command: %v\n", err) + } + } + + switch req.Code() { + case codes.POST, codes.DELETE, codes.PUT, codes.GET: + fnc(req, client) + case codes.Empty: + if !ok { + client.sendErrorResponse(fmt.Errorf("cannot handle command: client not found"), codes.InternalServerError, req.Token()) + closeClient(client) + return + } + case codes.Content: + // Unregistered observer at a peer send us a notification + default: + fmt.Printf("received invalid code: CoapCode(%v)", req.Code()) + } + }(writer, request) +} + +func defaultHandler(req *mux.Message, client *Client) { + path, _ := req.Options().Path() + client.sendErrorResponse(fmt.Errorf("DeviceId: %v: unknown path %v", client.GetDeviceID(), path), codes.NotFound, req.Token()) +} + +func (s *Service) setupCoapServer() error { + setHandlerError := func(uri string, err error) error { + return fmt.Errorf("failed to set %v handler: %w", uri, err) + } + m := mux.NewRouter() + m.DefaultHandle(mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + validateCommand(w, r, s, defaultHandler) + })) + if err := m.Handle(ocfCloud.SignUp, mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + validateCommand(w, r, s, signUpHandler) + })); err != nil { + return setHandlerError(ocfCloud.SignUp, err) + } + if err := m.Handle(ocfCloud.SignIn, mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + validateCommand(w, r, s, signInHandler) + })); err != nil { + return setHandlerError(ocfCloud.SignIn, err) + } + if err := m.Handle(ocfCloud.ResourceDirectory, mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + validateCommand(w, r, s, resourceDirectoryHandler) + })); err != nil { + return setHandlerError(ocfCloud.ResourceDirectory, err) + } + if err := m.Handle(ocfCloud.RefreshToken, mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + validateCommand(w, r, s, refreshTokenHandler) + })); err != nil { + return setHandlerError(ocfCloud.RefreshToken, err) + } + + opts := make([]coapTcpServer.Option, 0, 5) + opts = append(opts, options.WithOnNewConn(s.coapConnOnNew)) + opts = append(opts, options.WithMux(m)) + opts = append(opts, options.WithContext(s.ctx)) + opts = append(opts, options.WithErrors(func(e error) { + fmt.Printf("test-coap: %v\n", e) + })) + opts = append(opts, options.WithMaxMessageSize(bridgeNet.DefaultMaxMessageSize)) + s.coapServer = tcp.NewServer(opts...) + return nil +} + +func (s *Service) Serve() error { + return s.serveWithHandlingSignal() +} + +func (s *Service) serveWithHandlingSignal() error { + var wg sync.WaitGroup + var err error + wg.Add(1) + go func(server *Service) { + defer wg.Done() + err = server.coapServer.Serve(server.listener) + server.cancel() + for i := range server.closeFn { + server.closeFn[len(server.closeFn)-1-i]() + } + }(s) + + signal.Notify(s.sigs, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + <-s.sigs + + s.coapServer.Stop() + wg.Wait() + + return err +} + +func (s *Service) GetClients() []*Client { + return s.clients +} + +// Close turns off the server. +func (s *Service) Close() error { + select { + case s.sigs <- syscall.SIGTERM: + default: + } + return nil +} diff --git a/test/coap-gateway/service/serviceHandler.go b/test/coap-gateway/service/serviceHandler.go new file mode 100644 index 00000000..a53bf793 --- /dev/null +++ b/test/coap-gateway/service/serviceHandler.go @@ -0,0 +1,65 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + "github.com/plgd-dev/go-coap/v3/tcp/client" +) + +type ServiceHandlerConfig struct { + coapConn *client.Conn +} + +func (s *ServiceHandlerConfig) GetCoapConnection() *client.Conn { + return s.coapConn +} + +type Option interface { + Apply(o *ServiceHandlerConfig) +} + +type CoapConnectionOpt struct { + coapConn *client.Conn +} + +func (o CoapConnectionOpt) Apply(opts *ServiceHandlerConfig) { + opts.coapConn = o.coapConn +} + +func WithCoapConnectionOpt(c *client.Conn) CoapConnectionOpt { + return CoapConnectionOpt{ + coapConn: c, + } +} + +type GetServiceHandler = func(service *Service, opts ...Option) ServiceHandler + +type OnShutdown = func(ServiceHandler) + +type ServiceHandler interface { + CloseOnError() bool + SignUp(req ocfCloud.CoapSignUpRequest) (ocfCloud.CoapSignUpResponse, error) + SignOff() error + SignIn(req ocfCloud.CoapSignInRequest) (ocfCloud.CoapSignInResponse, error) + SignOut(req ocfCloud.CoapSignInRequest) error + PublishResources(req ocfCloud.PublishResourcesRequest) error + UnpublishResources(req ocfCloud.UnpublishResourcesRequest) error + RefreshToken(req ocfCloud.CoapRefreshTokenRequest) (ocfCloud.CoapRefreshTokenResponse, error) +} diff --git a/test/coap-gateway/service/signIn.go b/test/coap-gateway/service/signIn.go new file mode 100644 index 00000000..3ec9b23b --- /dev/null +++ b/test/coap-gateway/service/signIn.go @@ -0,0 +1,95 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + "fmt" + + "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/kit/v2/codec/cbor" +) + +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.session.swagger.json +func signInPostHandler(req *mux.Message, client *Client, signIn cloud.CoapSignInRequest) { + logErrorAndCloseClient := func(err error, code coapCodes.Code) { + client.sendErrorResponse(fmt.Errorf("cannot handle sign in: %w", err), code, req.Token()) + if client.handler == nil || client.handler.CloseOnError() { + if err := client.Close(); err != nil { + fmt.Printf("sign in error: %v\n", err) + } + } + } + + resp, err := client.handler.SignIn(signIn) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + out, err := cbor.Encode(resp) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + client.sendResponse(coapCodes.Changed, req.Token(), out) +} + +// Sign-Out +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.session.swagger.json +func signOutPostHandler(req *mux.Message, client *Client, signOut cloud.CoapSignInRequest) { + logErrorAndCloseClient := func(err error, code coapCodes.Code) { + client.sendErrorResponse(fmt.Errorf("cannot handle sign out: %w", err), code, req.Token()) + if client.handler == nil || client.handler.CloseOnError() { + if err := client.Close(); err != nil { + fmt.Printf("sign out error: %v\n", err) + } + } + } + + err := client.handler.SignOut(signOut) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + client.sendResponse(coapCodes.Changed, req.Token(), []byte{0xA0}) // empty object +} + +// Sign-in +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.session.swagger.json +func signInHandler(req *mux.Message, client *Client) { + if req.Code() == coapCodes.POST { + var r cloud.CoapSignInRequest + err := cbor.ReadFrom(req.Body(), &r) + if err != nil { + client.sendErrorResponse(fmt.Errorf("cannot handle sign in: %w", err), coapCodes.BadRequest, req.Token()) + return + } + if r.Login { + signInPostHandler(req, client, r) + return + } + signOutPostHandler(req, client, r) + return + } + client.sendErrorResponse(fmt.Errorf("forbidden request from %v", client.RemoteAddrString()), coapCodes.Forbidden, req.Token()) +} diff --git a/test/coap-gateway/service/signUp.go b/test/coap-gateway/service/signUp.go new file mode 100644 index 00000000..be7509eb --- /dev/null +++ b/test/coap-gateway/service/signUp.go @@ -0,0 +1,96 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service + +import ( + "fmt" + + "github.com/plgd-dev/device/v2/pkg/ocf/cloud" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/kit/v2/codec/cbor" +) + +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.account.swagger.json +func signUpPostHandler(r *mux.Message, client *Client) { + logErrorAndCloseClient := func(err error, code coapCodes.Code) { + client.sendErrorResponse(fmt.Errorf("cannot handle sign up: %w", err), code, r.Token()) + if client.handler == nil || client.handler.CloseOnError() { + if err := client.Close(); err != nil { + fmt.Printf("sign up error: %v\n", err) + } + } + } + + var signUp cloud.CoapSignUpRequest + if err := cbor.ReadFrom(r.Body(), &signUp); err != nil { + logErrorAndCloseClient(err, coapCodes.BadRequest) + return + } + + client.SetDeviceID(signUp.DeviceID) + + resp, err := client.handler.SignUp(signUp) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + out, err := cbor.Encode(resp) + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + client.sendResponse(coapCodes.Changed, r.Token(), out) +} + +// Sign-off +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.account.swagger.json +func signOffHandler(req *mux.Message, client *Client) { + logErrorAndCloseClient := func(err error, code coapCodes.Code) { + client.sendErrorResponse(fmt.Errorf("cannot handle sign off: %w", err), code, req.Token()) + if client.handler == nil || client.handler.CloseOnError() { + if err := client.Close(); err != nil { + fmt.Printf("sign off error: %v\n", err) + } + } + } + + err := client.handler.SignOff() + if err != nil { + logErrorAndCloseClient(err, coapCodes.InternalServerError) + return + } + + client.sendResponse(coapCodes.Deleted, req.Token(), nil) +} + +// Sign-up +// https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.account.swagger.json +func signUpHandler(r *mux.Message, client *Client) { + switch r.Code() { + case coapCodes.POST: + signUpPostHandler(r, client) + case coapCodes.DELETE: + signOffHandler(r, client) + default: + client.sendErrorResponse(fmt.Errorf("forbidden request from %v", client.RemoteAddrString()), coapCodes.Forbidden, r.Token()) + } +} diff --git a/test/coap-gateway/test.go b/test/coap-gateway/test.go new file mode 100644 index 00000000..2d57543c --- /dev/null +++ b/test/coap-gateway/test.go @@ -0,0 +1,69 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package test + +import ( + "context" + "crypto/tls" + "sync" + "testing" + + "github.com/plgd-dev/device/v2/test" + "github.com/plgd-dev/device/v2/test/coap-gateway/service" + "github.com/stretchr/testify/require" +) + +const ( + COAP_GW_HOST = "localhost:21002" +) + +func MakeConfig(t *testing.T) service.Config { + return service.Config{ + Addr: COAP_GW_HOST, + TLS: service.TLSConfig{ + Enabled: true, + Config: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec + Certificates: []tls.Certificate{test.GetCoapCertificate(t)}, + }, + }, + } +} + +func New(t *testing.T, getHandler service.GetServiceHandler, onShutdown service.OnShutdown) func() { + ctx := context.Background() + s, err := service.New(ctx, MakeConfig(t), getHandler) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _ = s.Serve() + }() + return func() { + _ = s.Close() + wg.Wait() + if onShutdown != nil { + for _, c := range s.GetClients() { + onShutdown(c.GetServiceHandler()) + } + } + } +} diff --git a/test/ocfbridge/main.go b/test/ocfbridge/main.go index 94a0fe49..25cb97f0 100644 --- a/test/ocfbridge/main.go +++ b/test/ocfbridge/main.go @@ -23,8 +23,8 @@ func testConfig() service.Config { CoAP: service.CoAPConfig{ ID: uuid.New().String(), Config: net.Config{ - ExternalAddress: "127.0.0.1:15683", - MaxMessageSize: 2097152, + ExternalAddresses: []string{"127.0.0.1:15683", "[::1]:15683"}, + MaxMessageSize: 2097152, }, }, }, diff --git a/test/test.go b/test/test.go index e522c750..25982401 100644 --- a/test/test.go +++ b/test/test.go @@ -19,6 +19,7 @@ package test import ( "context" "crypto" + "crypto/tls" "crypto/x509" "fmt" "os" @@ -95,7 +96,7 @@ func (h *findDeviceIDByNameHandler) Handle(ctx context.Context, dev *core.Device } func (h *findDeviceIDByNameHandler) Error(err error) { - log.Debug(err) + fmt.Printf("%v\n", err) } func FindDeviceByName(ctx context.Context, name string) (deviceID string, _ error) { @@ -287,3 +288,27 @@ func CheckResourceLinks(t *testing.T, expected, actual schema.ResourceLinks) { } require.Empty(t, expLinks) } + +func CloudID() string { + return os.Getenv("CLOUD_SID") +} + +func GetRootCertificate(t *testing.T) tls.Certificate { + certPath := os.Getenv("ROOT_CA_CRT") + require.NotEmpty(t, certPath) + keyPath := os.Getenv("ROOT_CA_KEY") + require.NotEmpty(t, keyPath) + ca, err := tls.LoadX509KeyPair(certPath, keyPath) + require.NoError(t, err) + return ca +} + +func GetCoapCertificate(t *testing.T) tls.Certificate { + certPath := os.Getenv("COAP_CRT") + require.NotEmpty(t, certPath) + keyPath := os.Getenv("COAP_KEY") + require.NotEmpty(t, keyPath) + ca, err := tls.LoadX509KeyPair(certPath, keyPath) + require.NoError(t, err) + return ca +}