// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package api import ( "context" "encoding/json" "io/ioutil" "net/http" "net/url" "regexp" "strings" "time" kitot "github.com/go-kit/kit/tracing/opentracing" kithttp "github.com/go-kit/kit/transport/http" "github.com/go-zoo/bone" "github.com/mainflux/mainflux" adapter "github.com/mainflux/mainflux/http" "github.com/mainflux/mainflux/internal/apiutil" "github.com/mainflux/mainflux/logger" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux/pkg/messaging" opentracing "github.com/opentracing/opentracing-go" "github.com/prometheus/client_golang/prometheus/promhttp" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const ( protocol = "http" ctSenmlJSON = "application/senml+json" ctSenmlCBOR = "application/senml+cbor" ctJSON = "application/json" ) var ( errMalformedSubtopic = errors.New("malformed subtopic") ) var channelPartRegExp = regexp.MustCompile(`^/channels/([\w\-]+)/messages(/[^?]*)?(\?.*)?$`) // MakeHandler returns a HTTP handler for API endpoints. func MakeHandler(svc adapter.Service, tracer opentracing.Tracer, logger logger.Logger) http.Handler { opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(encodeError), } r := bone.New() r.Post("/channels/:chanID/messages", kithttp.NewServer( kitot.TraceServer(tracer, "publish")(sendMessageEndpoint(svc)), decodeRequest, encodeResponse, opts..., )) r.Post("/channels/:chanID/messages/*", kithttp.NewServer( kitot.TraceServer(tracer, "publish")(sendMessageEndpoint(svc)), decodeRequest, encodeResponse, opts..., )) r.GetFunc("/health", mainflux.Health("http")) r.Handle("/metrics", promhttp.Handler()) return r } 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 } func decodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { ct := r.Header.Get("Content-Type") if ct != ctSenmlJSON && ct != ctJSON && ct != ctSenmlCBOR { return nil, errors.ErrUnsupportedContentType } channelParts := channelPartRegExp.FindStringSubmatch(r.RequestURI) if len(channelParts) < 2 { return nil, errors.ErrMalformedEntity } subtopic, err := parseSubtopic(channelParts[2]) if err != nil { return nil, err } var token string _, pass, ok := r.BasicAuth() switch { case ok: token = pass case !ok: token = apiutil.ExtractThingKey(r) } payload, err := ioutil.ReadAll(r.Body) if err != nil { return nil, errors.ErrMalformedEntity } defer r.Body.Close() req := publishReq{ msg: &messaging.Message{ Protocol: protocol, Channel: bone.GetValue(r, "chanID"), Subtopic: subtopic, Payload: payload, Created: time.Now().UnixNano(), }, token: token, } return req, nil } func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { w.WriteHeader(http.StatusAccepted) return nil } func encodeError(_ context.Context, err error, w http.ResponseWriter) { switch { case errors.Contains(err, errors.ErrAuthentication), err == apiutil.ErrBearerKey, err == apiutil.ErrBearerToken: w.WriteHeader(http.StatusUnauthorized) case errors.Contains(err, errors.ErrAuthorization): w.WriteHeader(http.StatusForbidden) case errors.Contains(err, errors.ErrUnsupportedContentType): w.WriteHeader(http.StatusUnsupportedMediaType) case errors.Contains(err, errMalformedSubtopic), errors.Contains(err, errors.ErrMalformedEntity): w.WriteHeader(http.StatusBadRequest) default: switch e, ok := status.FromError(err); { case ok: switch e.Code() { case codes.Unauthenticated: w.WriteHeader(http.StatusUnauthorized) case codes.PermissionDenied: w.WriteHeader(http.StatusForbidden) case codes.Internal: w.WriteHeader(http.StatusInternalServerError) case codes.NotFound: err = errors.ErrNotFound w.WriteHeader(http.StatusNotFound) default: w.WriteHeader(http.StatusInternalServerError) } default: w.WriteHeader(http.StatusInternalServerError) } } if errorVal, ok := err.(errors.Error); ok { w.Header().Set("Content-Type", ctJSON) if err := json.NewEncoder(w).Encode(apiutil.ErrorRes{Err: errorVal.Msg()}); err != nil { w.WriteHeader(http.StatusInternalServerError) } } }