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  }