github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/coprocess.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "net/url" 7 "reflect" 8 "strings" 9 "time" 10 "unicode/utf8" 11 12 "github.com/sirupsen/logrus" 13 14 "github.com/TykTechnologies/tyk/apidef" 15 "github.com/TykTechnologies/tyk/config" 16 "github.com/TykTechnologies/tyk/coprocess" 17 18 "errors" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 ) 23 24 var ( 25 supportedDrivers = []apidef.MiddlewareDriver{apidef.PythonDriver, apidef.LuaDriver, apidef.GrpcDriver} 26 loadedDrivers = map[apidef.MiddlewareDriver]coprocess.Dispatcher{} 27 ) 28 29 // CoProcessMiddleware is the basic CP middleware struct. 30 type CoProcessMiddleware struct { 31 BaseMiddleware 32 HookType coprocess.HookType 33 HookName string 34 MiddlewareDriver apidef.MiddlewareDriver 35 RawBodyOnly bool 36 37 successHandler *SuccessHandler 38 } 39 40 func (m *CoProcessMiddleware) Name() string { 41 return "CoProcessMiddleware" 42 } 43 44 // CreateCoProcessMiddleware initializes a new CP middleware, takes hook type (pre, post, etc.), hook name ("my_hook") and driver ("python"). 45 func CreateCoProcessMiddleware(hookName string, hookType coprocess.HookType, mwDriver apidef.MiddlewareDriver, baseMid BaseMiddleware) func(http.Handler) http.Handler { 46 dMiddleware := &CoProcessMiddleware{ 47 BaseMiddleware: baseMid, 48 HookType: hookType, 49 HookName: hookName, 50 MiddlewareDriver: mwDriver, 51 successHandler: &SuccessHandler{baseMid}, 52 } 53 54 return createMiddleware(dMiddleware) 55 } 56 57 func DoCoprocessReload() { 58 log.WithFields(logrus.Fields{ 59 "prefix": "coprocess", 60 }).Info("Reloading middlewares") 61 if dispatcher := loadedDrivers[apidef.PythonDriver]; dispatcher != nil { 62 dispatcher.Reload() 63 } 64 } 65 66 // CoProcessor represents a CoProcess during the request. 67 type CoProcessor struct { 68 HookType coprocess.HookType 69 Middleware *CoProcessMiddleware 70 } 71 72 // ObjectFromRequest constructs a CoProcessObject from a given http.Request. 73 func (c *CoProcessor) ObjectFromRequest(r *http.Request) (*coprocess.Object, error) { 74 headers := ProtoMap(r.Header) 75 76 host := r.Host 77 if host == "" && r.URL != nil { 78 host = r.URL.Host 79 } 80 if host != "" { 81 headers["Host"] = host 82 } 83 scheme := "http" 84 if r.TLS != nil { 85 scheme = "https" 86 } 87 miniRequestObject := &coprocess.MiniRequestObject{ 88 Headers: headers, 89 SetHeaders: map[string]string{}, 90 DeleteHeaders: []string{}, 91 Url: r.URL.String(), 92 Params: ProtoMap(r.URL.Query()), 93 AddParams: map[string]string{}, 94 ExtendedParams: ProtoMap(nil), 95 DeleteParams: []string{}, 96 ReturnOverrides: &coprocess.ReturnOverrides{ 97 ResponseCode: -1, 98 }, 99 Method: r.Method, 100 RequestUri: r.RequestURI, 101 Scheme: scheme, 102 } 103 104 if r.Body != nil { 105 defer r.Body.Close() 106 var err error 107 miniRequestObject.RawBody, err = ioutil.ReadAll(r.Body) 108 if err != nil { 109 return nil, err 110 } 111 if utf8.Valid(miniRequestObject.RawBody) && !c.Middleware.RawBodyOnly { 112 miniRequestObject.Body = string(miniRequestObject.RawBody) 113 } 114 } 115 116 object := &coprocess.Object{ 117 Request: miniRequestObject, 118 HookName: c.Middleware.HookName, 119 } 120 121 // If a middleware is set, take its HookType, otherwise override it with CoProcessor.HookType 122 if c.Middleware != nil && c.HookType == 0 { 123 c.HookType = c.Middleware.HookType 124 } 125 126 object.HookType = c.HookType 127 128 object.Spec = make(map[string]string) 129 130 // Append spec data: 131 if c.Middleware != nil { 132 configDataAsJSON := []byte("{}") 133 if len(c.Middleware.Spec.ConfigData) > 0 { 134 var err error 135 configDataAsJSON, err = json.Marshal(c.Middleware.Spec.ConfigData) 136 if err != nil { 137 return nil, err 138 } 139 } 140 141 object.Spec = map[string]string{ 142 "OrgID": c.Middleware.Spec.OrgID, 143 "APIID": c.Middleware.Spec.APIID, 144 "config_data": string(configDataAsJSON), 145 } 146 } 147 148 // Encode the session object (if not a pre-process & not a custom key check): 149 if c.HookType != coprocess.HookType_Pre && c.HookType != coprocess.HookType_CustomKeyCheck { 150 if session := ctxGetSession(r); session != nil { 151 object.Session = ProtoSessionState(session) 152 // For compatibility purposes: 153 object.Metadata = object.Session.GetMetadata() 154 } 155 } 156 157 return object, nil 158 } 159 160 // ObjectPostProcess does CoProcessObject post-processing (adding/removing headers or params, etc.). 161 func (c *CoProcessor) ObjectPostProcess(object *coprocess.Object, r *http.Request, origURL string, origMethod string) (err error) { 162 r.ContentLength = int64(len(object.Request.RawBody)) 163 r.Body = ioutil.NopCloser(bytes.NewReader(object.Request.RawBody)) 164 nopCloseRequestBody(r) 165 166 logger := c.Middleware.Logger() 167 168 for _, dh := range object.Request.DeleteHeaders { 169 r.Header.Del(dh) 170 } 171 172 for h, v := range object.Request.SetHeaders { 173 r.Header.Set(h, v) 174 } 175 176 updatedValues := r.URL.Query() 177 for _, k := range object.Request.DeleteParams { 178 updatedValues.Del(k) 179 } 180 181 for p, v := range object.Request.AddParams { 182 updatedValues.Set(p, v) 183 } 184 185 parsedURL, err := url.ParseRequestURI(object.Request.Url) 186 if err != nil { 187 logger.Error(err) 188 return 189 } 190 191 rewriteURL := ctxGetURLRewriteTarget(r) 192 if rewriteURL != nil { 193 ctxSetURLRewriteTarget(r, parsedURL) 194 r.URL, err = url.ParseRequestURI(origURL) 195 if err != nil { 196 logger.Error(err) 197 return 198 } 199 } else { 200 r.URL = parsedURL 201 } 202 203 transformMethod := ctxGetTransformRequestMethod(r) 204 if transformMethod != "" { 205 ctxSetTransformRequestMethod(r, object.Request.Method) 206 r.Method = origMethod 207 } else { 208 r.Method = object.Request.Method 209 } 210 211 if !reflect.DeepEqual(r.URL.Query(), updatedValues) { 212 r.URL.RawQuery = updatedValues.Encode() 213 } 214 215 return 216 } 217 218 // CoProcessInit creates a new CoProcessDispatcher, it will be called when Tyk starts. 219 func CoProcessInit() { 220 if !config.Global().CoProcessOptions.EnableCoProcess { 221 log.WithFields(logrus.Fields{ 222 "prefix": "coprocess", 223 }).Info("Rich plugins are disabled") 224 return 225 } 226 227 // Load gRPC dispatcher: 228 if config.Global().CoProcessOptions.CoProcessGRPCServer != "" { 229 var err error 230 loadedDrivers[apidef.GrpcDriver], err = NewGRPCDispatcher() 231 if err == nil { 232 log.WithFields(logrus.Fields{ 233 "prefix": "coprocess", 234 }).Info("gRPC dispatcher was initialized") 235 } else { 236 log.WithFields(logrus.Fields{ 237 "prefix": "coprocess", 238 }).WithError(err).Error("Couldn't load gRPC dispatcher") 239 } 240 } 241 } 242 243 // EnabledForSpec checks if this middleware should be enabled for a given API. 244 func (m *CoProcessMiddleware) EnabledForSpec() bool { 245 if !config.Global().CoProcessOptions.EnableCoProcess { 246 log.WithFields(logrus.Fields{ 247 "prefix": "coprocess", 248 }).Error("Your API specifies a CP custom middleware, either Tyk wasn't build with CP support or CP is not enabled in your Tyk configuration file!") 249 return false 250 } 251 252 var supported bool 253 for _, driver := range supportedDrivers { 254 if m.Spec.CustomMiddleware.Driver == driver { 255 supported = true 256 } 257 } 258 259 if !supported { 260 log.WithFields(logrus.Fields{ 261 "prefix": "coprocess", 262 }).Debug("Enabling CP middleware.") 263 m.successHandler = &SuccessHandler{m.BaseMiddleware} 264 return true 265 } 266 267 if d, _ := loadedDrivers[m.Spec.CustomMiddleware.Driver]; d == nil { 268 log.WithFields(logrus.Fields{ 269 "prefix": "coprocess", 270 }).Errorf("Driver '%s' isn't loaded", m.Spec.CustomMiddleware.Driver) 271 return false 272 } 273 274 log.WithFields(logrus.Fields{ 275 "prefix": "coprocess", 276 }).Debug("Enabling CP middleware.") 277 m.successHandler = &SuccessHandler{m.BaseMiddleware} 278 return true 279 } 280 281 // ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail 282 func (m *CoProcessMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { 283 logger := m.Logger() 284 logger.Debug("CoProcess Request, HookType: ", m.HookType) 285 originalURL := r.URL 286 287 var extractor IdExtractor 288 if m.Spec.EnableCoProcessAuth && m.Spec.CustomMiddleware.IdExtractor.Extractor != nil { 289 extractor = m.Spec.CustomMiddleware.IdExtractor.Extractor.(IdExtractor) 290 } 291 292 var returnOverrides ReturnOverrides 293 var sessionID string 294 295 if m.HookType == coprocess.HookType_CustomKeyCheck && extractor != nil { 296 sessionID, returnOverrides = extractor.ExtractAndCheck(r) 297 298 if returnOverrides.ResponseCode != 0 { 299 if returnOverrides.ResponseError == "" { 300 return nil, returnOverrides.ResponseCode 301 } 302 err := errors.New(returnOverrides.ResponseError) 303 return err, returnOverrides.ResponseCode 304 } 305 } 306 307 // It's also possible to override the HookType: 308 coProcessor := CoProcessor{ 309 Middleware: m, 310 // HookType: coprocess.PreHook, 311 } 312 313 object, err := coProcessor.ObjectFromRequest(r) 314 if err != nil { 315 logger.WithError(err).Error("Failed to build request object") 316 return errors.New("Middleware error"), 500 317 } 318 319 var origURL string 320 if rewriteUrl := ctxGetURLRewriteTarget(r); rewriteUrl != nil { 321 origURL = object.Request.Url 322 object.Request.Url = rewriteUrl.String() 323 object.Request.RequestUri = rewriteUrl.RequestURI() 324 } 325 326 var origMethod string 327 if transformMethod := ctxGetTransformRequestMethod(r); transformMethod != "" { 328 origMethod = r.Method 329 object.Request.Method = transformMethod 330 } 331 332 t1 := time.Now() 333 returnObject, err := coProcessor.Dispatch(object) 334 t2 := time.Now() 335 336 if err != nil { 337 logger.WithError(err).Error("Dispatch error") 338 if m.HookType == coprocess.HookType_CustomKeyCheck { 339 return errors.New("Key not authorised"), 403 340 } else { 341 return errors.New("Middleware error"), 500 342 } 343 } 344 345 ms := float64(t2.UnixNano()-t1.UnixNano()) * 0.000001 346 m.logger.WithField("ms", ms).Debug("gRPC request processing took") 347 348 err = coProcessor.ObjectPostProcess(returnObject, r, origURL, origMethod) 349 if err != nil { 350 // Restore original URL object so that it can be used by ErrorHandler: 351 r.URL = originalURL 352 logger.WithError(err).Error("Failed to post-process request object") 353 return errors.New("Middleware error"), 500 354 } 355 356 var token string 357 if returnObject.Session != nil { 358 // For compatibility purposes, inject coprocess.Object.Metadata fields: 359 if returnObject.Metadata != nil { 360 if returnObject.Session.GetMetadata() == nil { 361 returnObject.Session.Metadata = make(map[string]string) 362 } 363 for k, v := range returnObject.GetMetadata() { 364 returnObject.Session.Metadata[k] = v 365 } 366 } 367 368 token = returnObject.Session.GetMetadata()["token"] 369 } 370 371 if returnObject.Request.ReturnOverrides.ResponseError != "" { 372 returnObject.Request.ReturnOverrides.ResponseBody = returnObject.Request.ReturnOverrides.ResponseError 373 } 374 375 // The CP middleware indicates this is a bad auth: 376 if returnObject.Request.ReturnOverrides.ResponseCode >= http.StatusBadRequest && !returnObject.Request.ReturnOverrides.OverrideError { 377 logger.WithField("key", obfuscateKey(token)).Info("Attempted access with invalid key") 378 379 for h, v := range returnObject.Request.ReturnOverrides.Headers { 380 w.Header().Set(h, v) 381 } 382 383 // Fire Authfailed Event 384 AuthFailed(m, r, token) 385 386 // Report in health check 387 reportHealthValue(m.Spec, KeyFailure, "1") 388 389 errorMsg := "Key not authorised" 390 if returnObject.Request.ReturnOverrides.ResponseBody != "" { 391 errorMsg = returnObject.Request.ReturnOverrides.ResponseBody 392 } 393 394 return errors.New(errorMsg), int(returnObject.Request.ReturnOverrides.ResponseCode) 395 } 396 397 if returnObject.Request.ReturnOverrides.ResponseCode > 0 { 398 for h, v := range returnObject.Request.ReturnOverrides.Headers { 399 w.Header().Set(h, v) 400 } 401 w.WriteHeader(int(returnObject.Request.ReturnOverrides.ResponseCode)) 402 w.Write([]byte(returnObject.Request.ReturnOverrides.ResponseBody)) 403 404 // Record analytics data: 405 res := new(http.Response) 406 res.Proto = "HTTP/1.0" 407 res.ProtoMajor = 1 408 res.ProtoMinor = 0 409 res.StatusCode = int(returnObject.Request.ReturnOverrides.ResponseCode) 410 res.Body = nopCloser{ 411 ReadSeeker: strings.NewReader(returnObject.Request.ReturnOverrides.ResponseBody), 412 } 413 res.ContentLength = int64(len(returnObject.Request.ReturnOverrides.ResponseBody)) 414 m.successHandler.RecordHit(r, Latency{Total: int64(ms)}, int(returnObject.Request.ReturnOverrides.ResponseCode), res) 415 return nil, mwStatusRespond 416 } 417 418 // Is this a CP authentication middleware? 419 if m.Spec.EnableCoProcessAuth && m.HookType == coprocess.HookType_CustomKeyCheck { 420 if extractor == nil { 421 sessionID = token 422 } 423 424 // The CP middleware didn't setup a session: 425 if returnObject.Session == nil || token == "" { 426 authHeaderValue, _ := m.getAuthToken(m.getAuthType(), r) 427 AuthFailed(m, r, authHeaderValue) 428 return errors.New(http.StatusText(http.StatusForbidden)), http.StatusForbidden 429 } 430 431 returnedSession := TykSessionState(returnObject.Session) 432 433 // If the returned object contains metadata, add them to the session: 434 for k, v := range returnObject.Metadata { 435 returnedSession.SetMetaDataKey(k, string(v)) 436 } 437 returnedSession.OrgID = m.Spec.OrgID 438 439 if err := m.ApplyPolicies(returnedSession); err != nil { 440 AuthFailed(m, r, r.Header.Get(m.Spec.Auth.AuthHeaderName)) 441 return errors.New(http.StatusText(http.StatusForbidden)), http.StatusForbidden 442 } 443 444 existingSession, found := FallbackKeySesionManager.SessionDetail(sessionID, false) 445 446 if found { 447 returnedSession.QuotaRenews = existingSession.QuotaRenews 448 returnedSession.QuotaRemaining = existingSession.QuotaRemaining 449 450 for api := range returnedSession.GetAccessRights() { 451 if _, found := existingSession.GetAccessRightByAPIID(api); found { 452 if returnedSession.GetAccessRights()[api].Limit != nil { 453 returnedSession.AccessRights[api].Limit.QuotaRenews = existingSession.GetAccessRights()[api].Limit.QuotaRenews 454 returnedSession.AccessRights[api].Limit.QuotaRemaining = existingSession.GetAccessRights()[api].Limit.QuotaRemaining 455 } 456 } 457 } 458 } 459 460 // Apply it second time to fix the quota 461 if err := m.ApplyPolicies(returnedSession); err != nil { 462 AuthFailed(m, r, r.Header.Get(m.Spec.Auth.AuthHeaderName)) 463 return errors.New(http.StatusText(http.StatusForbidden)), http.StatusForbidden 464 } 465 466 ctxSetSession(r, returnedSession, sessionID, true) 467 } 468 469 return nil, http.StatusOK 470 } 471 472 func (c *CoProcessor) Dispatch(object *coprocess.Object) (*coprocess.Object, error) { 473 dispatcher := loadedDrivers[c.Middleware.MiddlewareDriver] 474 if dispatcher == nil { 475 err := fmt.Errorf("Couldn't dispatch request, driver '%s' isn't available", c.Middleware.MiddlewareDriver) 476 return nil, err 477 } 478 newObject, err := dispatcher.Dispatch(object) 479 if err != nil { 480 return nil, err 481 } 482 return newObject, nil 483 }