package http import ( "context" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "strings" "github.com/gorilla/mux" actorErr "github.com/dapr/go-sdk/actor/error" "github.com/dapr/go-sdk/actor/runtime" "github.com/pkg/errors" "github.com/dapr/go-sdk/service/common" ) const ( // PubSubHandlerSuccessStatusCode is the successful ack code for pubsub event appcallback response. PubSubHandlerSuccessStatusCode int = http.StatusOK // PubSubHandlerRetryStatusCode is the error response code (nack) pubsub event appcallback response. PubSubHandlerRetryStatusCode int = http.StatusInternalServerError // PubSubHandlerDropStatusCode is the pubsub event appcallback response code indicating that Dapr should drop that message. PubSubHandlerDropStatusCode int = http.StatusSeeOther ) // topicEventJSON is identical to `common.TopicEvent` // except for it treats `data` as a json.RawMessage so it can // be used as bytes or interface{}. type topicEventJSON struct { // ID identifies the event. ID string `json:"id"` // The version of the CloudEvents specification. SpecVersion string `json:"specversion"` // The type of event related to the originating occurrence. Type string `json:"type"` // Source identifies the context in which an event happened. Source string `json:"source"` // The content type of data value. DataContentType string `json:"datacontenttype"` // The content of the event. // Note, this is why the gRPC and HTTP implementations need separate structs for cloud events. Data json.RawMessage `json:"data"` // The base64 encoding content of the event. // Note, this is processing rawPayload and binary content types. DataBase64 string `json:"data_base64,omitempty"` // Cloud event subject Subject string `json:"subject"` // The pubsub topic which publisher sent to. Topic string `json:"topic"` // PubsubName is name of the pub/sub this message came from PubsubName string `json:"pubsubname"` } func (s *Server) registerBaseHandler() { // register subscribe handler f := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(s.topicSubscriptions); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } s.mux.HandleFunc("/dapr/subscribe", f) // register health check handler fHealth := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } s.mux.HandleFunc("/healthz", fHealth).Methods(http.MethodGet) // register actor config handler fRegister := func(w http.ResponseWriter, r *http.Request) { data, err := runtime.GetActorRuntimeInstance().GetJSONSerializedConfig() if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if _, err = w.Write(data); err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } s.mux.HandleFunc("/dapr/config", fRegister).Methods(http.MethodGet) // register actor method invoke handler fInvoke := func(w http.ResponseWriter, r *http.Request) { varsMap := mux.Vars(r) actorType := varsMap["actorType"] actorID := varsMap["actorId"] methodName := varsMap["methodName"] reqData, _ := ioutil.ReadAll(r.Body) rspData, err := runtime.GetActorRuntimeInstance().InvokeActorMethod(actorType, actorID, methodName, reqData) if err == actorErr.ErrActorTypeNotFound { w.WriteHeader(http.StatusNotFound) return } if err != actorErr.Success { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(rspData) } s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/{methodName}", fInvoke).Methods(http.MethodPut) // register deactivate actor handler fDelete := func(w http.ResponseWriter, r *http.Request) { varsMap := mux.Vars(r) actorType := varsMap["actorType"] actorID := varsMap["actorId"] err := runtime.GetActorRuntimeInstance().Deactivate(actorType, actorID) if err == actorErr.ErrActorTypeNotFound || err == actorErr.ErrActorIDNotFound { w.WriteHeader(http.StatusNotFound) } if err != actorErr.Success { w.WriteHeader(http.StatusInternalServerError) } w.WriteHeader(http.StatusOK) } s.mux.HandleFunc("/actors/{actorType}/{actorId}", fDelete).Methods(http.MethodDelete) // register actor reminder invoke handler fReminder := func(w http.ResponseWriter, r *http.Request) { varsMap := mux.Vars(r) actorType := varsMap["actorType"] actorID := varsMap["actorId"] reminderName := varsMap["reminderName"] reqData, _ := ioutil.ReadAll(r.Body) err := runtime.GetActorRuntimeInstance().InvokeReminder(actorType, actorID, reminderName, reqData) if err == actorErr.ErrActorTypeNotFound { w.WriteHeader(http.StatusNotFound) } if err != actorErr.Success { w.WriteHeader(http.StatusInternalServerError) } w.WriteHeader(http.StatusOK) } s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/remind/{reminderName}", fReminder).Methods(http.MethodPut) // register actor timer invoke handler fTimer := func(w http.ResponseWriter, r *http.Request) { varsMap := mux.Vars(r) actorType := varsMap["actorType"] actorID := varsMap["actorId"] timerName := varsMap["timerName"] reqData, _ := ioutil.ReadAll(r.Body) err := runtime.GetActorRuntimeInstance().InvokeTimer(actorType, actorID, timerName, reqData) if err == actorErr.ErrActorTypeNotFound { w.WriteHeader(http.StatusNotFound) } if err != actorErr.Success { w.WriteHeader(http.StatusInternalServerError) } w.WriteHeader(http.StatusOK) } s.mux.HandleFunc("/actors/{actorType}/{actorId}/method/timer/{timerName}", fTimer).Methods(http.MethodPut) } // AddTopicEventHandler appends provided event handler with it's name to the service. func (s *Server) AddTopicEventHandler(sub *common.Subscription, fn func(ctx context.Context, e *common.TopicEvent) (retry bool, err error)) error { if sub == nil { return errors.New("subscription required") } if sub.Topic == "" { return errors.New("topic name required") } if sub.PubsubName == "" { return errors.New("pub/sub name required") } if sub.Route == "" { return errors.New("handler route name") } if fn == nil { return fmt.Errorf("topic handler required") } if !strings.HasPrefix(sub.Route, "/") { sub.Route = fmt.Sprintf("/%s", sub.Route) } s.topicSubscriptions = append(s.topicSubscriptions, sub) s.mux.Handle(sub.Route, optionsHandler(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // check for post with no data if r.ContentLength == 0 { http.Error(w, "nil content", PubSubHandlerDropStatusCode) return } // deserialize the event var in topicEventJSON if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), PubSubHandlerDropStatusCode) return } if in.PubsubName == "" { in.Topic = sub.PubsubName } if in.Topic == "" { in.Topic = sub.Topic } var data interface{} var rawData []byte if len(in.Data) > 0 { rawData = []byte(in.Data) data = rawData var v interface{} // We can assume that rawData is valid JSON // without checking in.DataContentType == "application/json". if err := json.Unmarshal(rawData, &v); err == nil { data = v // Handling of JSON base64 encoded or escaped in a string. if str, ok := v.(string); ok { // This is the path that will most likely succeed. var vString interface{} if err := json.Unmarshal([]byte(str), &vString); err == nil { data = vString } else if decoded, err := base64.StdEncoding.DecodeString(str); err == nil { // Decoded Base64 encoded JSON does not seem to be in the spec // but it is in existing unit tests so this handles that case. var vBase64 interface{} if err := json.Unmarshal(decoded, &vBase64); err == nil { data = vBase64 } } } } } else if in.DataBase64 != "" { var err error rawData, err = base64.StdEncoding.DecodeString(in.DataBase64) if err == nil { data = rawData if in.DataContentType == "application/json" { var v interface{} if err := json.Unmarshal(rawData, &v); err == nil { data = v } } } } te := common.TopicEvent{ ID: in.ID, SpecVersion: in.SpecVersion, Type: in.Type, Source: in.Source, DataContentType: in.DataContentType, Data: data, RawData: rawData, DataBase64: in.DataBase64, Subject: in.Subject, PubsubName: in.PubsubName, Topic: in.Topic, } w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // execute user handler retry, err := fn(r.Context(), &te) if err == nil { writeStatus(w, common.SubscriptionResponseStatusSuccess) return } if retry { writeStatus(w, common.SubscriptionResponseStatusRetry) return } writeStatus(w, common.SubscriptionResponseStatusDrop) }))) return nil } func writeStatus(w http.ResponseWriter, s string) { status := &common.SubscriptionResponse{Status: s} if err := json.NewEncoder(w).Encode(status); err != nil { http.Error(w, err.Error(), PubSubHandlerRetryStatusCode) } }