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 }