Skip to content

Commit

Permalink
feat: Accept URL escape for device command name and resource name
Browse files Browse the repository at this point in the history
- Use router.UseEncodedPath() to match the encoded original path to the routes
- Decode the Url path variable before passing to the controller

Close #4371

Signed-off-by: bruce <weichou1229@gmail.com>
  • Loading branch information
weichou1229 committed Feb 24, 2023
1 parent 76c26b6 commit a9b60e8
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 9 deletions.
10 changes: 8 additions & 2 deletions internal/core/command/controller/messaging/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package messaging
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"time"

Expand Down Expand Up @@ -110,15 +111,20 @@ func commandRequestHandler(requestTimeout time.Duration, dic *di.Container) mqtt

// expected external command request/response topic scheme: #/<device-name>/<command-name>/<method>
deviceName := topicLevels[length-3]
commandName := topicLevels[length-2]
commandName, err := url.QueryUnescape(topicLevels[length-2])
if err != nil {
lc.Errorf("Failed to unescape command name '%s': %s", commandName, err.Error())
lc.Warn("Not publishing error message back due to insufficient information on response topic")
return
}
method := topicLevels[length-1]
if !strings.EqualFold(method, "get") && !strings.EqualFold(method, "set") {
lc.Errorf("Unknown request method: %s, only 'get' or 'set' is allowed", method)
lc.Warn("Not publishing error message back due to insufficient information on response topic")
return
}

externalResponseTopic := strings.Join([]string{externalMQTTInfo.Topics[common.ExternalCommandResponseTopicPrefixKey], deviceName, commandName, method}, "/")
externalResponseTopic := common.BuildTopic(externalMQTTInfo.Topics[common.ExternalCommandResponseTopicPrefixKey], deviceName, url.QueryEscape(commandName), method)

internalBaseTopic := container.ConfigurationFrom(dic.Get).MessageBus.GetBaseTopicPrefix()
topicPrefix := common.BuildTopic(internalBaseTopic, common.CoreCommandDeviceRequestPublishTopic)
Expand Down
13 changes: 12 additions & 1 deletion internal/core/command/controller/messaging/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package messaging
import (
"context"
"fmt"
"net/url"
"strings"
"time"

Expand Down Expand Up @@ -98,7 +99,17 @@ func processDeviceCommandRequest(

// expected internal command request/response topic scheme: #/<device>/<command-name>/<method>
deviceName := topicLevels[length-3]
commandName := topicLevels[length-2]
commandName, err := url.QueryUnescape(topicLevels[length-2])
if err != nil {
err = fmt.Errorf("failed to unescape command name '%s': %s", commandName, err.Error())
lc.Error(err.Error())
responseEnvelope := types.NewMessageEnvelopeWithError(requestEnvelope.RequestID, err.Error())
err = messageBus.Publish(responseEnvelope, internalResponseTopic)
if err != nil {
lc.Errorf("Could not publish to topic '%s': %s", internalResponseTopic, err.Error())
}
return
}
method := topicLevels[length-1]
if !strings.EqualFold(method, "get") && !strings.EqualFold(method, "set") {
err = fmt.Errorf("unknown request method: %s, only 'get' or 'set' is allowed", method)
Expand Down
3 changes: 2 additions & 1 deletion internal/core/command/controller/messaging/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"

bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
Expand Down Expand Up @@ -47,7 +48,7 @@ func validateRequestTopic(prefix string, deviceName string, commandName string,
}

// expected internal command request topic scheme: <prefix>/<device-service>/<device>/<command-name>/<method>
return deviceServiceResponse.Service.Name, common.BuildTopic(prefix, deviceServiceResponse.Service.Name, deviceName, commandName, method), nil
return deviceServiceResponse.Service.Name, common.BuildTopic(prefix, deviceServiceResponse.Service.Name, deviceName, url.QueryEscape(commandName), method), nil

}

Expand Down
5 changes: 4 additions & 1 deletion internal/core/command/router.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (C) 2021 IOTech Ltd
// Copyright (C) 2021-2023 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -19,6 +19,8 @@ import (
)

func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {
// r.UseEncodedPath() tells the router to match the encoded original path to the routes
r.UseEncodedPath()
// Common
cc := commonController.NewCommonController(dic, serviceName)
r.HandleFunc(common.ApiPingRoute, cc.Ping).Methods(http.MethodGet)
Expand All @@ -34,4 +36,5 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {

r.Use(correlation.ManageHeader)
r.Use(correlation.LoggingMiddleware(container.LoggingClientFrom(dic.Get)))
r.Use(correlation.UrlDecodeMiddleware(container.LoggingClientFrom(dic.Get)))
}
3 changes: 2 additions & 1 deletion internal/core/data/application/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package application
import (
"context"
"fmt"
"net/url"
"strings"

msgTypes "github.com/edgexfoundry/go-mod-messaging/v3/pkg/types"
Expand Down Expand Up @@ -82,7 +83,7 @@ func (a *CoreDataApp) PublishEvent(data []byte, serviceName string, profileName
correlationId := correlation.FromContext(ctx)

basePrefix := configuration.MessageBus.GetBaseTopicPrefix()
publishTopic := common.BuildTopic(basePrefix, common.EventsPublishTopic, CoreDataEventTopicPrefix, serviceName, profileName, deviceName, sourceName)
publishTopic := common.BuildTopic(basePrefix, common.EventsPublishTopic, CoreDataEventTopicPrefix, serviceName, profileName, deviceName, url.QueryEscape(sourceName))
lc.Debugf("Publishing AddEventRequest to MessageBus. Topic: %s; %s: %s", publishTopic, common.CorrelationHeader, correlationId)

msgEnvelope := msgTypes.NewMessageEnvelope(data, ctx)
Expand Down
8 changes: 6 additions & 2 deletions internal/core/data/controller/messaging/subscriber.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 All @@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"

cbor "github.com/fxamacker/cbor/v2"
Expand Down Expand Up @@ -119,7 +120,10 @@ func validateEvent(messageTopic string, e dtos.Event) errors.EdgeX {
len := len(fields)
profileName := fields[len-3]
deviceName := fields[len-2]
sourceName := fields[len-1]
sourceName, err := url.QueryUnescape(fields[len-1])
if err != nil {
return errors.NewCommonEdgeXWrapper(err)
}

// Check whether the event fields match the message topic
if e.ProfileName != profileName {
Expand Down
3 changes: 3 additions & 0 deletions internal/core/data/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
)

func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {
// r.UseEncodedPath() tells the router to match the encoded original path to the routes
r.UseEncodedPath()
// Common
cc := commonController.NewCommonController(dic, serviceName)
r.HandleFunc(common.ApiPingRoute, cc.Ping).Methods(http.MethodGet)
Expand Down Expand Up @@ -54,4 +56,5 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {

r.Use(correlation.ManageHeader)
r.Use(correlation.LoggingMiddleware(container.LoggingClientFrom(dic.Get)))
r.Use(correlation.UrlDecodeMiddleware(container.LoggingClientFrom(dic.Get)))
}
5 changes: 4 additions & 1 deletion internal/core/metadata/router.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 All @@ -20,6 +20,8 @@ import (
)

func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {
// r.UseEncodedPath() tells the router to match the encoded original path to the routes
r.UseEncodedPath()
// Common
cc := commonController.NewCommonController(dic, serviceName)
r.HandleFunc(common.ApiPingRoute, cc.Ping).Methods(http.MethodGet)
Expand Down Expand Up @@ -88,4 +90,5 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container, serviceName string) {

r.Use(correlation.ManageHeader)
r.Use(correlation.LoggingMiddleware(container.LoggingClientFrom(dic.Get)))
r.Use(correlation.UrlDecodeMiddleware(container.LoggingClientFrom(dic.Get)))
}
21 changes: 21 additions & 0 deletions internal/pkg/correlation/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package correlation

import (
"context"
"github.com/gorilla/mux"
"net/http"
"net/url"
"time"

"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
Expand Down Expand Up @@ -47,3 +49,22 @@ func LoggingMiddleware(lc logger.LoggingClient) func(http.Handler) http.Handler
})
}
}

// UrlDecodeMiddleware decode the path variables
// After invoking the router.UseEncodedPath() func, the path variables needs to decode before passing to the controller
func UrlDecodeMiddleware(lc logger.LoggingClient) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
for k, v := range vars {
unescape, err := url.QueryUnescape(v)
if err != nil {
lc.Debugf("failed to decode the %s from the value %s", k, v)
return
}
vars[k] = unescape
}
next.ServeHTTP(w, r)
})
}
}

0 comments on commit a9b60e8

Please sign in to comment.