213 lines
5.0 KiB
Go
213 lines
5.0 KiB
Go
// Copyright (c) Mainflux
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mainflux/mainflux/pkg/errors"
|
|
|
|
"github.com/go-zoo/bone"
|
|
"github.com/mainflux/mainflux"
|
|
"github.com/mainflux/mainflux/coap"
|
|
log "github.com/mainflux/mainflux/logger"
|
|
"github.com/mainflux/mainflux/pkg/messaging"
|
|
"github.com/plgd-dev/go-coap/v2/message"
|
|
"github.com/plgd-dev/go-coap/v2/message/codes"
|
|
"github.com/plgd-dev/go-coap/v2/mux"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
const (
|
|
protocol = "coap"
|
|
authQuery = "auth"
|
|
startObserve = 0 // observe option value that indicates start of observation
|
|
)
|
|
|
|
var channelPartRegExp = regexp.MustCompile(`^/channels/([\w\-]+)/messages(/[^?]*)?(\?.*)?$`)
|
|
|
|
const (
|
|
numGroups = 3 // entire expression + channel group + subtopic group
|
|
channelGroup = 2 // channel group is second in channel regexp
|
|
)
|
|
|
|
var (
|
|
errMalformedSubtopic = errors.New("malformed subtopic")
|
|
errBadOptions = errors.New("bad options")
|
|
)
|
|
|
|
var (
|
|
logger log.Logger
|
|
service coap.Service
|
|
)
|
|
|
|
// MakeHandler returns a HTTP handler for API endpoints.
|
|
func MakeHTTPHandler() http.Handler {
|
|
b := bone.New()
|
|
b.GetFunc("/health", mainflux.Health(protocol))
|
|
b.Handle("/metrics", promhttp.Handler())
|
|
|
|
return b
|
|
}
|
|
|
|
// MakeCoAPHandler creates handler for CoAP messages.
|
|
func MakeCoAPHandler(svc coap.Service, l log.Logger) mux.HandlerFunc {
|
|
logger = l
|
|
service = svc
|
|
|
|
return handler
|
|
}
|
|
|
|
func sendResp(w mux.ResponseWriter, resp *message.Message) {
|
|
if err := w.Client().WriteMessage(resp); err != nil {
|
|
logger.Warn(fmt.Sprintf("Can't set response: %s", err))
|
|
}
|
|
}
|
|
|
|
func handler(w mux.ResponseWriter, m *mux.Message) {
|
|
resp := message.Message{
|
|
Code: codes.Content,
|
|
Token: m.Token,
|
|
Context: m.Context,
|
|
Options: make(message.Options, 0, 16),
|
|
}
|
|
|
|
msg, err := decodeMessage(m)
|
|
if err != nil {
|
|
logger.Warn(fmt.Sprintf("Error decoding message: %s", err))
|
|
resp.Code = codes.BadRequest
|
|
sendResp(w, &resp)
|
|
return
|
|
}
|
|
key, err := parseKey(m)
|
|
if err != nil {
|
|
logger.Warn(fmt.Sprintf("Error parsing auth: %s", err))
|
|
resp.Code = codes.Unauthorized
|
|
sendResp(w, &resp)
|
|
return
|
|
}
|
|
switch m.Code {
|
|
case codes.GET:
|
|
err = handleGet(m, w.Client(), msg, key)
|
|
case codes.POST:
|
|
err = service.Publish(context.Background(), key, msg)
|
|
default:
|
|
err = errors.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
switch {
|
|
case err == errBadOptions:
|
|
resp.Code = codes.BadOption
|
|
case err == errors.ErrNotFound:
|
|
resp.Code = codes.NotFound
|
|
case errors.Contains(err, errors.ErrAuthorization),
|
|
errors.Contains(err, errors.ErrAuthentication):
|
|
resp.Code = codes.Unauthorized
|
|
default:
|
|
resp.Code = codes.InternalServerError
|
|
}
|
|
sendResp(w, &resp)
|
|
}
|
|
}
|
|
|
|
func handleGet(m *mux.Message, c mux.Client, msg messaging.Message, key string) error {
|
|
var obs uint32
|
|
obs, err := m.Options.Observe()
|
|
if err != nil {
|
|
logger.Warn(fmt.Sprintf("Error reading observe option: %s", err))
|
|
return errBadOptions
|
|
}
|
|
if obs == startObserve {
|
|
c := coap.NewClient(c, m.Token, logger)
|
|
return service.Subscribe(context.Background(), key, msg.Channel, msg.Subtopic, c)
|
|
}
|
|
return service.Unsubscribe(context.Background(), key, msg.Channel, msg.Subtopic, m.Token.String())
|
|
}
|
|
|
|
func decodeMessage(msg *mux.Message) (messaging.Message, error) {
|
|
if msg.Options == nil {
|
|
return messaging.Message{}, errBadOptions
|
|
}
|
|
path, err := msg.Options.Path()
|
|
if err != nil {
|
|
return messaging.Message{}, err
|
|
}
|
|
channelParts := channelPartRegExp.FindStringSubmatch(path)
|
|
if len(channelParts) < numGroups {
|
|
return messaging.Message{}, errMalformedSubtopic
|
|
}
|
|
|
|
st, err := parseSubtopic(channelParts[channelGroup])
|
|
if err != nil {
|
|
return messaging.Message{}, err
|
|
}
|
|
ret := messaging.Message{
|
|
Protocol: protocol,
|
|
Channel: channelParts[1],
|
|
Subtopic: st,
|
|
Payload: []byte{},
|
|
Created: time.Now().UnixNano(),
|
|
}
|
|
|
|
if msg.Body != nil {
|
|
buff, err := ioutil.ReadAll(msg.Body)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
ret.Payload = buff
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func parseKey(msg *mux.Message) (string, error) {
|
|
if obs, _ := msg.Options.Observe(); obs != 0 && msg.Code == codes.GET {
|
|
return "", nil
|
|
}
|
|
authKey, err := msg.Options.GetString(message.URIQuery)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
vars := strings.Split(authKey, "=")
|
|
if len(vars) != 2 || vars[0] != authQuery {
|
|
return "", errors.ErrAuthorization
|
|
}
|
|
return vars[1], nil
|
|
}
|
|
|
|
func parseSubtopic(subtopic string) (string, error) {
|
|
if subtopic == "" {
|
|
return subtopic, nil
|
|
}
|
|
|
|
subtopic, err := url.QueryUnescape(subtopic)
|
|
if err != nil {
|
|
return "", errMalformedSubtopic
|
|
}
|
|
subtopic = strings.ReplaceAll(subtopic, "/", ".")
|
|
|
|
elems := strings.Split(subtopic, ".")
|
|
filteredElems := []string{}
|
|
for _, elem := range elems {
|
|
if elem == "" {
|
|
continue
|
|
}
|
|
|
|
if len(elem) > 1 && (strings.Contains(elem, "*") || strings.Contains(elem, ">")) {
|
|
return "", errMalformedSubtopic
|
|
}
|
|
|
|
filteredElems = append(filteredElems, elem)
|
|
}
|
|
|
|
subtopic = strings.Join(filteredElems, ".")
|
|
return subtopic, nil
|
|
}
|