Skip to content

Commit

Permalink
feat: Use URL escape for device command name and resource name
Browse files Browse the repository at this point in the history
Remove the constraint of device command name and resource name, and use URL escape in client API.

Close #773

Signed-off-by: bruce <weichou1229@gmail.com>
  • Loading branch information
weichou1229 committed Feb 17, 2023
1 parent 1091de3 commit 8c1f421
Show file tree
Hide file tree
Showing 12 changed files with 54 additions and 42 deletions.
10 changes: 5 additions & 5 deletions clients/http/command.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2021-2022 IOTech Ltd
// Copyright (C) 2021-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -59,7 +59,7 @@ func (client *CommandClient) IssueGetCommandByName(ctx context.Context, deviceNa
requestParams := url.Values{}
requestParams.Set(common.PushEvent, dsPushEvent)
requestParams.Set(common.ReturnEvent, dsReturnEvent)
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
err = utils.GetRequest(ctx, &res, client.baseUrl, requestPath, requestParams)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
Expand All @@ -73,7 +73,7 @@ func (client *CommandClient) IssueGetCommandByNameWithQueryParams(ctx context.Co
requestParams.Set(k, v)
}

requestPath := path.Join(common.ApiDeviceRoute, common.Name, url.QueryEscape(deviceName), url.QueryEscape(commandName))
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
err = utils.GetRequest(ctx, &res, client.baseUrl, requestPath, requestParams)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
Expand All @@ -83,7 +83,7 @@ func (client *CommandClient) IssueGetCommandByNameWithQueryParams(ctx context.Co

// IssueSetCommandByName issues the specified write command referenced by the command name to the device/sensor that is also referenced by name.
func (client *CommandClient) IssueSetCommandByName(ctx context.Context, deviceName string, commandName string, settings map[string]string) (res dtoCommon.BaseResponse, err errors.EdgeX) {
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
err = utils.PutRequest(ctx, &res, client.baseUrl, requestPath, nil, settings)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
Expand All @@ -93,7 +93,7 @@ func (client *CommandClient) IssueSetCommandByName(ctx context.Context, deviceNa

// IssueSetCommandByNameWithObject issues the specified write command and the settings supports object value type
func (client *CommandClient) IssueSetCommandByNameWithObject(ctx context.Context, deviceName string, commandName string, settings map[string]interface{}) (res dtoCommon.BaseResponse, err errors.EdgeX) {
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
err = utils.PutRequest(ctx, &res, client.baseUrl, requestPath, nil, settings)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
Expand Down
8 changes: 4 additions & 4 deletions clients/http/deviceprofile.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020-2022 IOTech Ltd
// Copyright (C) 2020-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -159,7 +159,7 @@ func (client *DeviceProfileClient) DeviceResourceByProfileNameAndResourceName(ct
if exists {
return res, nil
}
requestPath := path.Join(common.ApiDeviceResourceRoute, common.Profile, profileName, common.Resource, resourceName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceResourceRoute, common.Profile, profileName, common.Resource, resourceName)
err := utils.GetRequest(ctx, &res, client.baseUrl, requestPath, nil)
if err != nil {
return res, errors.NewCommonEdgeXWrapper(err)
Expand Down Expand Up @@ -220,7 +220,7 @@ func (client *DeviceProfileClient) UpdateDeviceProfileResource(ctx context.Conte
// DeleteDeviceResourceByName deletes device resource by name
func (client *DeviceProfileClient) DeleteDeviceResourceByName(ctx context.Context, profileName string, resourceName string) (dtoCommon.BaseResponse, errors.EdgeX) {
var response dtoCommon.BaseResponse
requestPath := path.Join(common.ApiDeviceProfileRoute, common.Name, url.QueryEscape(profileName), common.Resource, url.QueryEscape(resourceName))
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceProfileRoute, common.Name, profileName, common.Resource, resourceName)
err := utils.DeleteRequest(ctx, &response, client.baseUrl, requestPath)
if err != nil {
return response, errors.NewCommonEdgeXWrapper(err)
Expand Down Expand Up @@ -251,7 +251,7 @@ func (client *DeviceProfileClient) UpdateDeviceProfileDeviceCommand(ctx context.
// DeleteDeviceCommandByName deletes device command by name
func (client *DeviceProfileClient) DeleteDeviceCommandByName(ctx context.Context, profileName string, commandName string) (dtoCommon.BaseResponse, errors.EdgeX) {
var response dtoCommon.BaseResponse
requestPath := path.Join(common.ApiDeviceProfileRoute, common.Name, url.QueryEscape(profileName), common.DeviceCommand, url.QueryEscape(commandName))
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceProfileRoute, common.Name, profileName, common.DeviceCommand, commandName)
err := utils.DeleteRequest(ctx, &response, client.baseUrl, requestPath)
if err != nil {
return response, errors.NewCommonEdgeXWrapper(err)
Expand Down
12 changes: 5 additions & 7 deletions clients/http/deviceservicecommand.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2021 IOTech Ltd
// Copyright (C) 2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -8,15 +8,13 @@ package http
import (
"context"
"encoding/json"
"net/url"
"path"

"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/http/utils"
"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/interfaces"
"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
dtoCommon "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common"
"github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses"
"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
"net/url"

"github.com/fxamacker/cbor/v2"
)
Expand All @@ -30,7 +28,7 @@ func NewDeviceServiceCommandClient() interfaces.DeviceServiceCommandClient {

// GetCommand sends HTTP request to execute the Get command
func (client *deviceServiceCommandClient) GetCommand(ctx context.Context, baseUrl string, deviceName string, commandName string, queryParams string) (*responses.EventResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
params, err := url.ParseQuery(queryParams)
if err != nil {
return nil, errors.NewCommonEdgeXWrapper(err)
Expand Down Expand Up @@ -60,7 +58,7 @@ func (client *deviceServiceCommandClient) GetCommand(ctx context.Context, baseUr
// SetCommand sends HTTP request to execute the Set command
func (client *deviceServiceCommandClient) SetCommand(ctx context.Context, baseUrl string, deviceName string, commandName string, queryParams string, settings map[string]string) (dtoCommon.BaseResponse, errors.EdgeX) {
var response dtoCommon.BaseResponse
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
params, err := url.ParseQuery(queryParams)
if err != nil {
return response, errors.NewCommonEdgeXWrapper(err)
Expand All @@ -75,7 +73,7 @@ func (client *deviceServiceCommandClient) SetCommand(ctx context.Context, baseUr
// SetCommandWithObject invokes device service's set command API and the settings supports object value type
func (client *deviceServiceCommandClient) SetCommandWithObject(ctx context.Context, baseUrl string, deviceName string, commandName string, queryParams string, settings map[string]interface{}) (dtoCommon.BaseResponse, errors.EdgeX) {
var response dtoCommon.BaseResponse
requestPath := path.Join(common.ApiDeviceRoute, common.Name, deviceName, commandName)
requestPath := utils.EscapeAndJoinPath(common.ApiDeviceRoute, common.Name, deviceName, commandName)
params, err := url.ParseQuery(queryParams)
if err != nil {
return response, errors.NewCommonEdgeXWrapper(err)
Expand Down
12 changes: 6 additions & 6 deletions clients/http/reading.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020-2021 IOTech Ltd
// Copyright (C) 2020-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -75,7 +75,7 @@ func (rc readingClient) ReadingsByDeviceName(ctx context.Context, name string, o
}

func (rc readingClient) ReadingsByResourceName(ctx context.Context, name string, offset, limit int) (responses.MultiReadingsResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiReadingRoute, common.ResourceName, name)
requestPath := utils.EscapeAndJoinPath(common.ApiReadingRoute, common.ResourceName, name)
requestParams := url.Values{}
requestParams.Set(common.Offset, strconv.Itoa(offset))
requestParams.Set(common.Limit, strconv.Itoa(limit))
Expand All @@ -102,7 +102,7 @@ func (rc readingClient) ReadingsByTimeRange(ctx context.Context, start, end, off

// ReadingsByResourceNameAndTimeRange returns readings by resource name and specified time range. Readings are sorted in descending order of origin time.
func (rc readingClient) ReadingsByResourceNameAndTimeRange(ctx context.Context, name string, start, end, offset, limit int) (responses.MultiReadingsResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiReadingRoute, common.ResourceName, name, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestPath := utils.EscapeAndJoinPath(common.ApiReadingRoute, common.ResourceName, name, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestParams := url.Values{}
requestParams.Set(common.Offset, strconv.Itoa(offset))
requestParams.Set(common.Limit, strconv.Itoa(limit))
Expand All @@ -115,7 +115,7 @@ func (rc readingClient) ReadingsByResourceNameAndTimeRange(ctx context.Context,
}

func (rc readingClient) ReadingsByDeviceNameAndResourceName(ctx context.Context, deviceName, resourceName string, offset, limit int) (responses.MultiReadingsResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.ResourceName, resourceName)
requestPath := utils.EscapeAndJoinPath(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.ResourceName, resourceName)
requestParams := url.Values{}
requestParams.Set(common.Offset, strconv.Itoa(offset))
requestParams.Set(common.Limit, strconv.Itoa(limit))
Expand All @@ -129,7 +129,7 @@ func (rc readingClient) ReadingsByDeviceNameAndResourceName(ctx context.Context,
}

func (rc readingClient) ReadingsByDeviceNameAndResourceNameAndTimeRange(ctx context.Context, deviceName, resourceName string, start, end, offset, limit int) (responses.MultiReadingsResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.ResourceName, resourceName, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestPath := utils.EscapeAndJoinPath(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.ResourceName, resourceName, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestParams := url.Values{}
requestParams.Set(common.Offset, strconv.Itoa(offset))
requestParams.Set(common.Limit, strconv.Itoa(limit))
Expand All @@ -142,7 +142,7 @@ func (rc readingClient) ReadingsByDeviceNameAndResourceNameAndTimeRange(ctx cont
}

func (rc readingClient) ReadingsByDeviceNameAndResourceNamesAndTimeRange(ctx context.Context, deviceName string, resourceNames []string, start, end, offset, limit int) (responses.MultiReadingsResponse, errors.EdgeX) {
requestPath := path.Join(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestPath := utils.EscapeAndJoinPath(common.ApiReadingRoute, common.Device, common.Name, deviceName, common.Start, strconv.Itoa(start), common.End, strconv.Itoa(end))
requestParams := url.Values{}
requestParams.Set(common.Offset, strconv.Itoa(offset))
requestParams.Set(common.Limit, strconv.Itoa(limit))
Expand Down
32 changes: 26 additions & 6 deletions clients/http/utils/common.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020-2021 IOTech Ltd
// Copyright (C) 2020-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -15,6 +15,7 @@ import (
"mime/multipart"
"net/http"
"net/url"
"path"
"path/filepath"

"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
Expand Down Expand Up @@ -74,7 +75,7 @@ func createRequest(ctx context.Context, httpMethod string, baseUrl string, reque
if requestParams != nil {
u.RawQuery = requestParams.Encode()
}
req, err := http.NewRequest(httpMethod, u.String(), nil)
req, err := http.NewRequest(httpMethod, edgeXClientReqURI(u), nil)
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create a http request", err)
}
Expand All @@ -101,7 +102,7 @@ func createRequestWithRawDataAndParams(ctx context.Context, httpMethod string, b
content = common.ContentTypeJSON
}

req, err := http.NewRequest(httpMethod, u.String(), bytes.NewReader(jsonEncodedData))
req, err := http.NewRequest(httpMethod, edgeXClientReqURI(u), bytes.NewReader(jsonEncodedData))
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create a http request", err)
}
Expand Down Expand Up @@ -130,7 +131,7 @@ func createRequestWithRawData(ctx context.Context, httpMethod string, baseUrl st
content = common.ContentTypeJSON
}

req, err := http.NewRequest(httpMethod, u.String(), bytes.NewReader(jsonEncodedData))
req, err := http.NewRequest(httpMethod, edgeXClientReqURI(u), bytes.NewReader(jsonEncodedData))
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create a http request", err)
}
Expand All @@ -151,7 +152,7 @@ func createRequestWithEncodedData(ctx context.Context, httpMethod string, baseUr
content = FromContext(ctx, common.ContentType)
}

req, err := http.NewRequest(httpMethod, u.String(), bytes.NewReader(data))
req, err := http.NewRequest(httpMethod, edgeXClientReqURI(u), bytes.NewReader(data))
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create a http request", err)
}
Expand Down Expand Up @@ -185,7 +186,7 @@ func createRequestFromFilePath(ctx context.Context, httpMethod string, baseUrl s
}
writer.Close()

req, err := http.NewRequest(httpMethod, u.String(), body)
req, err := http.NewRequest(httpMethod, edgeXClientReqURI(u), body)
if err != nil {
return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create a http request", err)
}
Expand Down Expand Up @@ -217,3 +218,22 @@ func sendRequest(ctx context.Context, req *http.Request) ([]byte, errors.EdgeX)
errKind := errors.KindMapping(resp.StatusCode)
return nil, errors.NewCommonEdgeX(errKind, msg, nil)
}

// EscapeAndJoinPath escape and join the path variables
func EscapeAndJoinPath(apiRoutePath string, pathVariables ...string) string {
elements := make([]string, len(pathVariables)+1)
elements[0] = apiRoutePath // we don't need to escape the route path like /device, /reading, ...,etc.
for i, e := range pathVariables {
elements[i+1] = url.QueryEscape(e)
}
return path.Join(elements...)
}

// edgeXClientReqURI returns the non-encoded path?query that would be used in an HTTP request for u.
func edgeXClientReqURI(u *url.URL) string {
result := u.Scheme + "://" + u.Host + u.Path
if u.ForceQuery || u.RawQuery != "" {
result += "?" + u.RawQuery
}
return result
}
2 changes: 1 addition & 1 deletion common/utils.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2020 IOTech Ltd
// Copyright (C) 2020-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand Down
2 changes: 1 addition & 1 deletion dtos/corecommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type DeviceCoreCommand struct {
// CoreCommand and its properties are defined in the APIv2 specification:
// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-command/2.1.0#/CoreCommand
type CoreCommand struct {
Name string `json:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"`
Name string `json:"name" validate:"required,edgex-dto-none-empty-strin"`
Get bool `json:"get,omitempty" validate:"required_without=Set"`
Set bool `json:"set,omitempty" validate:"required_without=Get"`
Path string `json:"path,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions dtos/devicecommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import "github.com/edgexfoundry/go-mod-core-contracts/v2/models"
// DeviceCommand and its properties are defined in the APIv2 specification:
// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-metadata/2.1.0#/DeviceCommand
type DeviceCommand struct {
Name string `json:"name" yaml:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"`
Name string `json:"name" yaml:"name" validate:"required,edgex-dto-none-empty-string"`
IsHidden bool `json:"isHidden" yaml:"isHidden"`
ReadWrite string `json:"readWrite" yaml:"readWrite" validate:"required,oneof='R' 'W' 'RW' 'WR'"`
ResourceOperations []ResourceOperation `json:"resourceOperations" yaml:"resourceOperations" validate:"gt=0,dive"`
Expand All @@ -19,7 +19,7 @@ type DeviceCommand struct {
// UpdateDeviceComman and its properties are defined in the APIv2 specification:
// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-metadata/2.2.0#/DeviceCommand
type UpdateDeviceCommand struct {
Name *string `json:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"`
Name *string `json:"name" validate:"required,edgex-dto-none-empty-string"`
IsHidden *bool `json:"isHidden"`
}

Expand Down
4 changes: 2 additions & 2 deletions dtos/deviceresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "github.com/edgexfoundry/go-mod-core-contracts/v2/models"
// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-metadata/2.1.0#/DeviceResource
type DeviceResource struct {
Description string `json:"description" yaml:"description"`
Name string `json:"name" yaml:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"`
Name string `json:"name" yaml:"name" validate:"required,edgex-dto-none-empty-string"`
IsHidden bool `json:"isHidden" yaml:"isHidden"`
Tag string `json:"tag" yaml:"tag"`
Properties ResourceProperties `json:"properties" yaml:"properties"`
Expand All @@ -22,7 +22,7 @@ type DeviceResource struct {
// https://app.swaggerhub.com/apis-docs/EdgeXFoundry1/core-metadata/2.2.0#/DeviceResource
type UpdateDeviceResource struct {
Description *string `json:"description"`
Name *string `json:"name" validate:"required,edgex-dto-none-empty-string,edgex-dto-rfc3986-unreserved-chars"`
Name *string `json:"name" validate:"required,edgex-dto-none-empty-string"`
IsHidden *bool `json:"isHidden"`
}

Expand Down
2 changes: 1 addition & 1 deletion dtos/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Event struct {
Id string `json:"id" validate:"required,uuid"`
DeviceName string `json:"deviceName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
ProfileName string `json:"profileName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
SourceName string `json:"sourceName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
SourceName string `json:"sourceName" validate:"required"`
Origin int64 `json:"origin" validate:"required"`
Readings []BaseReading `json:"readings" validate:"gt=0,dive,required"`
Tags map[string]interface{} `json:"tags,omitempty" xml:"-"` // Have to ignore since map not supported for XML
Expand Down
2 changes: 1 addition & 1 deletion dtos/reading.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type BaseReading struct {
Id string `json:"id,omitempty"`
Origin int64 `json:"origin" validate:"required"`
DeviceName string `json:"deviceName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
ResourceName string `json:"resourceName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
ResourceName string `json:"resourceName" validate:"required"`
ProfileName string `json:"profileName" validate:"required,edgex-dto-rfc3986-unreserved-chars"`
ValueType string `json:"valueType" validate:"required,edgex-dto-value-type"`
Units string `json:"units,omitempty"`
Expand Down
Loading

0 comments on commit 8c1f421

Please sign in to comment.