gitee.com/liuxuezhan/go-micro-v1.18.0@v1.0.0/api/handler/cloudevents/event.go (about)

     1  /*
     2   * From: https://github.com/serverless/event-gateway/blob/master/event/event.go
     3   * Modified: Strip to handler requirements
     4   *
     5   * Copyright 2017 Serverless, Inc.
     6   *
     7   * Licensed under the Apache License, Version 2.0 (the "License");
     8   * you may not use this file except in compliance with the License.
     9   * You may obtain a copy of the License at
    10   *
    11   *     http://www.apache.org/licenses/LICENSE-2.0
    12   *
    13   * Unless required by applicable law or agreed to in writing, software
    14   * distributed under the License is distributed on an "AS IS" BASIS,
    15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16   * See the License for the specific language governing permissions and
    17   * limitations under the License.
    18   *
    19   */
    20  
    21  package cloudevents
    22  
    23  import (
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"mime"
    29  	"net/http"
    30  	"strings"
    31  	"time"
    32  	"unicode"
    33  
    34  	"github.com/google/uuid"
    35  	validator "gopkg.in/go-playground/validator.v9"
    36  )
    37  
    38  const (
    39  	// TransformationVersion is indicative of the revision of how Event Gateway transforms a request into CloudEvents format.
    40  	TransformationVersion = "0.1"
    41  
    42  	// CloudEventsVersion currently supported by Event Gateway
    43  	CloudEventsVersion = "0.1"
    44  )
    45  
    46  // Event is a default event structure. All data that passes through the Event Gateway
    47  // is formatted to a format defined CloudEvents v0.1 spec.
    48  type Event struct {
    49  	EventType          string                 `json:"eventType" validate:"required"`
    50  	EventTypeVersion   string                 `json:"eventTypeVersion,omitempty"`
    51  	CloudEventsVersion string                 `json:"cloudEventsVersion" validate:"required"`
    52  	Source             string                 `json:"source" validate:"uri,required"`
    53  	EventID            string                 `json:"eventID" validate:"required"`
    54  	EventTime          *time.Time             `json:"eventTime,omitempty"`
    55  	SchemaURL          string                 `json:"schemaURL,omitempty"`
    56  	Extensions         map[string]interface{} `json:"extensions,omitempty"`
    57  	ContentType        string                 `json:"contentType,omitempty"`
    58  	Data               interface{}            `json:"data"`
    59  }
    60  
    61  // New return new instance of Event.
    62  func New(eventType string, mimeType string, payload interface{}) *Event {
    63  	now := time.Now()
    64  
    65  	event := &Event{
    66  		EventType:          eventType,
    67  		CloudEventsVersion: CloudEventsVersion,
    68  		Source:             "https://micro.mu",
    69  		EventID:            uuid.New().String(),
    70  		EventTime:          &now,
    71  		ContentType:        mimeType,
    72  		Data:               payload,
    73  		Extensions: map[string]interface{}{
    74  			"eventgateway": map[string]interface{}{
    75  				"transformed":            "true",
    76  				"transformation-version": TransformationVersion,
    77  			},
    78  		},
    79  	}
    80  
    81  	event.Data = normalizePayload(event.Data, event.ContentType)
    82  	return event
    83  }
    84  
    85  // FromRequest takes an HTTP request and returns an Event along with path. Most of the implementation
    86  // is based on https://github.com/cloudevents/spec/blob/master/http-transport-binding.md.
    87  // This function also supports legacy mode where event type is sent in Event header.
    88  func FromRequest(r *http.Request) (*Event, error) {
    89  	contentType := r.Header.Get("Content-Type")
    90  	mimeType, _, err := mime.ParseMediaType(contentType)
    91  	if err != nil {
    92  		if err.Error() != "mime: no media type" {
    93  			return nil, err
    94  		}
    95  		mimeType = "application/octet-stream"
    96  	}
    97  	// Read request body
    98  	body := []byte{}
    99  	if r.Body != nil {
   100  		body, err = ioutil.ReadAll(r.Body)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  	}
   105  
   106  	var event *Event
   107  	if mimeType == mimeCloudEventsJSON { // CloudEvents Structured Content Mode
   108  		return parseAsCloudEvent(mimeType, body)
   109  	} else if isCloudEventsBinaryContentMode(r.Header) { // CloudEvents Binary Content Mode
   110  		return parseAsCloudEventBinary(r.Header, body)
   111  	} else if isLegacyMode(r.Header) {
   112  		if mimeType == mimeJSON { // CloudEvent in Legacy Mode
   113  			event, err = parseAsCloudEvent(mimeType, body)
   114  			if err != nil {
   115  				return New(string(r.Header.Get("event")), mimeType, body), nil
   116  			}
   117  			return event, err
   118  		}
   119  
   120  		return New(string(r.Header.Get("event")), mimeType, body), nil
   121  	}
   122  
   123  	return New("http.request", mimeJSON, newHTTPRequestData(r, body)), nil
   124  }
   125  
   126  // Validate Event struct
   127  func (e *Event) Validate() error {
   128  	validate := validator.New()
   129  	err := validate.Struct(e)
   130  	if err != nil {
   131  		return fmt.Errorf("CloudEvent not valid: %v", err)
   132  	}
   133  	return nil
   134  }
   135  
   136  func isLegacyMode(headers http.Header) bool {
   137  	if headers.Get("Event") != "" {
   138  		return true
   139  	}
   140  
   141  	return false
   142  }
   143  
   144  func isCloudEventsBinaryContentMode(headers http.Header) bool {
   145  	if headers.Get("CE-EventType") != "" &&
   146  		headers.Get("CE-CloudEventsVersion") != "" &&
   147  		headers.Get("CE-Source") != "" &&
   148  		headers.Get("CE-EventID") != "" {
   149  		return true
   150  	}
   151  
   152  	return false
   153  }
   154  
   155  func parseAsCloudEventBinary(headers http.Header, payload interface{}) (*Event, error) {
   156  	event := &Event{
   157  		EventType:          headers.Get("CE-EventType"),
   158  		EventTypeVersion:   headers.Get("CE-EventTypeVersion"),
   159  		CloudEventsVersion: headers.Get("CE-CloudEventsVersion"),
   160  		Source:             headers.Get("CE-Source"),
   161  		EventID:            headers.Get("CE-EventID"),
   162  		ContentType:        headers.Get("Content-Type"),
   163  		Data:               payload,
   164  	}
   165  
   166  	err := event.Validate()
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	if headers.Get("CE-EventTime") != "" {
   172  		val, err := time.Parse(time.RFC3339, headers.Get("CE-EventTime"))
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  		event.EventTime = &val
   177  	}
   178  
   179  	if val := headers.Get("CE-SchemaURL"); len(val) > 0 {
   180  		event.SchemaURL = val
   181  	}
   182  
   183  	event.Extensions = map[string]interface{}{}
   184  	for key, val := range flatten(headers) {
   185  		if strings.HasPrefix(key, "Ce-X-") {
   186  			key = strings.TrimLeft(key, "Ce-X-")
   187  			// Make first character lowercase
   188  			runes := []rune(key)
   189  			runes[0] = unicode.ToLower(runes[0])
   190  			event.Extensions[string(runes)] = val
   191  		}
   192  	}
   193  
   194  	event.Data = normalizePayload(event.Data, event.ContentType)
   195  	return event, nil
   196  }
   197  
   198  func flatten(h http.Header) map[string]string {
   199  	headers := map[string]string{}
   200  	for key, header := range h {
   201  		headers[key] = header[0]
   202  		if len(header) > 1 {
   203  			headers[key] = strings.Join(header, ", ")
   204  		}
   205  	}
   206  	return headers
   207  }
   208  
   209  func parseAsCloudEvent(mime string, payload interface{}) (*Event, error) {
   210  	body, ok := payload.([]byte)
   211  	if ok {
   212  		event := &Event{}
   213  		err := json.Unmarshal(body, event)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  
   218  		err = event.Validate()
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  
   223  		event.Data = normalizePayload(event.Data, event.ContentType)
   224  		return event, nil
   225  	}
   226  
   227  	return nil, errors.New("couldn't cast to []byte")
   228  }
   229  
   230  const (
   231  	mimeJSON            = "application/json"
   232  	mimeFormMultipart   = "multipart/form-data"
   233  	mimeFormURLEncoded  = "application/x-www-form-urlencoded"
   234  	mimeCloudEventsJSON = "application/cloudevents+json"
   235  )
   236  
   237  // normalizePayload takes anything, checks if it's []byte array and depending on provided mime
   238  // type converts it to either string or map[string]interface to avoid having base64 string after
   239  // JSON marshaling.
   240  func normalizePayload(payload interface{}, mime string) interface{} {
   241  	if bytePayload, ok := payload.([]byte); ok && len(bytePayload) > 0 {
   242  		switch {
   243  		case mime == mimeJSON || strings.HasSuffix(mime, "+json"):
   244  			var result map[string]interface{}
   245  			err := json.Unmarshal(bytePayload, &result)
   246  			if err != nil {
   247  				return payload
   248  			}
   249  			return result
   250  		case strings.HasPrefix(mime, mimeFormMultipart), mime == mimeFormURLEncoded:
   251  			return string(bytePayload)
   252  		}
   253  	}
   254  
   255  	return payload
   256  }
   257  
   258  // HTTPRequestData is a event schema used for sending events to HTTP subscriptions.
   259  type HTTPRequestData struct {
   260  	Headers map[string]string   `json:"headers"`
   261  	Query   map[string][]string `json:"query"`
   262  	Body    interface{}         `json:"body"`
   263  	Host    string              `json:"host"`
   264  	Path    string              `json:"path"`
   265  	Method  string              `json:"method"`
   266  	Params  map[string]string   `json:"params"`
   267  }
   268  
   269  // NewHTTPRequestData returns a new instance of HTTPRequestData
   270  func newHTTPRequestData(r *http.Request, eventData interface{}) *HTTPRequestData {
   271  	req := &HTTPRequestData{
   272  		Headers: flatten(r.Header),
   273  		Query:   r.URL.Query(),
   274  		Body:    eventData,
   275  		Host:    r.Host,
   276  		Path:    r.URL.Path,
   277  		Method:  r.Method,
   278  	}
   279  
   280  	req.Body = normalizePayload(req.Body, r.Header.Get("content-type"))
   281  	return req
   282  }