diff --git a/internal/core/command/controller/messaging/external.go b/internal/core/command/controller/messaging/external.go index 48724fce25..c720a7029d 100644 --- a/internal/core/command/controller/messaging/external.go +++ b/internal/core/command/controller/messaging/external.go @@ -9,6 +9,7 @@ package messaging import ( "encoding/json" "fmt" + "net/url" "strings" "time" @@ -110,7 +111,12 @@ func commandRequestHandler(requestTimeout time.Duration, dic *di.Container) mqtt // expected external command request/response topic scheme: #/// 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) @@ -118,7 +124,7 @@ func commandRequestHandler(requestTimeout time.Duration, dic *di.Container) mqtt 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) diff --git a/internal/core/command/controller/messaging/internal.go b/internal/core/command/controller/messaging/internal.go index 116198e3f7..6227e5f59b 100644 --- a/internal/core/command/controller/messaging/internal.go +++ b/internal/core/command/controller/messaging/internal.go @@ -9,6 +9,7 @@ package messaging import ( "context" "fmt" + "net/url" "strings" "time" @@ -98,7 +99,17 @@ func processDeviceCommandRequest( // expected internal command request/response topic scheme: #/// 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) diff --git a/internal/core/command/controller/messaging/utils.go b/internal/core/command/controller/messaging/utils.go index 002e9d2867..3abeec8f2b 100644 --- a/internal/core/command/controller/messaging/utils.go +++ b/internal/core/command/controller/messaging/utils.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strconv" bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container" @@ -47,7 +48,7 @@ func validateRequestTopic(prefix string, deviceName string, commandName string, } // expected internal command request topic scheme: //// - 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 } diff --git a/internal/core/command/router.go b/internal/core/command/router.go index 2b75cb74a2..d7951159f7 100644 --- a/internal/core/command/router.go +++ b/internal/core/command/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021 IOTech Ltd +// Copyright (C) 2021-2023 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -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) @@ -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))) } diff --git a/internal/core/data/application/event.go b/internal/core/data/application/event.go index 2593532aa6..1bacd742b5 100644 --- a/internal/core/data/application/event.go +++ b/internal/core/data/application/event.go @@ -8,6 +8,7 @@ package application import ( "context" "fmt" + "net/url" "strings" msgTypes "github.com/edgexfoundry/go-mod-messaging/v3/pkg/types" @@ -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) diff --git a/internal/core/data/controller/messaging/subscriber.go b/internal/core/data/controller/messaging/subscriber.go index 06c7c9609a..eec9d8e569 100644 --- a/internal/core/data/controller/messaging/subscriber.go +++ b/internal/core/data/controller/messaging/subscriber.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021-2022 IOTech Ltd +// Copyright (C) 2021-2023 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" "strings" cbor "github.com/fxamacker/cbor/v2" @@ -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 { diff --git a/internal/core/data/router.go b/internal/core/data/router.go index 2c0f6c10b4..2415b6be9a 100644 --- a/internal/core/data/router.go +++ b/internal/core/data/router.go @@ -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) @@ -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))) } diff --git a/internal/core/metadata/router.go b/internal/core/metadata/router.go index f4f881d590..4a703063d9 100644 --- a/internal/core/metadata/router.go +++ b/internal/core/metadata/router.go @@ -1,5 +1,5 @@ // -// Copyright (C) 2021-2022 IOTech Ltd +// Copyright (C) 2021-2023 IOTech Ltd // // SPDX-License-Identifier: Apache-2.0 @@ -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) @@ -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))) } diff --git a/internal/pkg/correlation/middleware.go b/internal/pkg/correlation/middleware.go index 4727b329e7..e05b001f59 100644 --- a/internal/pkg/correlation/middleware.go +++ b/internal/pkg/correlation/middleware.go @@ -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" @@ -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) + }) + } +}