github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/mw_virtual_endpoint.go (about)

     1  package gateway
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"errors"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"reflect"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/robertkrimen/otto"
    20  	_ "github.com/robertkrimen/otto/underscore"
    21  
    22  	"github.com/TykTechnologies/tyk/apidef"
    23  	"github.com/TykTechnologies/tyk/config"
    24  	"github.com/TykTechnologies/tyk/headers"
    25  	"github.com/TykTechnologies/tyk/user"
    26  
    27  	"github.com/sirupsen/logrus"
    28  )
    29  
    30  // RequestObject is marshalled to JSON string and passed into JSON middleware
    31  type RequestObject struct {
    32  	Headers map[string][]string
    33  	Body    string
    34  	URL     string
    35  	Params  map[string][]string
    36  	Scheme  string
    37  }
    38  
    39  type ResponseObject struct {
    40  	Body    string
    41  	Headers map[string]string
    42  	Code    int
    43  }
    44  
    45  type VMResponseObject struct {
    46  	Response    ResponseObject
    47  	SessionMeta map[string]string
    48  }
    49  
    50  // DynamicMiddleware is a generic middleware that will execute JS code before continuing
    51  type VirtualEndpoint struct {
    52  	BaseMiddleware
    53  	sh SuccessHandler
    54  }
    55  
    56  func (d *VirtualEndpoint) Name() string {
    57  	return "VirtualEndpoint"
    58  }
    59  
    60  func preLoadVirtualMetaCode(meta *apidef.VirtualMeta, j *JSVM) {
    61  	// the only call site uses (&foo, &bar) so meta and j won't be
    62  	// nil.
    63  	var src interface{}
    64  	switch meta.FunctionSourceType {
    65  	case "file":
    66  		j.Log.Debug("Loading JS Endpoint File: ", meta.FunctionSourceURI)
    67  		f, err := os.Open(meta.FunctionSourceURI)
    68  		if err != nil {
    69  			j.Log.WithError(err).Error("Failed to open Endpoint JS")
    70  			return
    71  		}
    72  		src = f
    73  	case "blob":
    74  		if config.Global().DisableVirtualPathBlobs {
    75  			j.Log.Error("[JSVM] Blobs not allowed on this node")
    76  			return
    77  		}
    78  		j.Log.Debug("Loading JS blob")
    79  		js, err := base64.StdEncoding.DecodeString(meta.FunctionSourceURI)
    80  		if err != nil {
    81  			j.Log.WithError(err).Error("Failed to load blob JS")
    82  			return
    83  		}
    84  		src = js
    85  	default:
    86  		j.Log.Error("Type must be either file or blob (base64)!")
    87  		return
    88  	}
    89  	if _, err := j.VM.Run(src); err != nil {
    90  		j.Log.WithError(err).Error("Could not load virtual endpoint JS")
    91  	}
    92  }
    93  
    94  func (d *VirtualEndpoint) Init() {
    95  	d.sh = SuccessHandler{d.BaseMiddleware}
    96  }
    97  
    98  func (d *VirtualEndpoint) EnabledForSpec() bool {
    99  	if !d.Spec.GlobalConfig.EnableJSVM {
   100  		return false
   101  	}
   102  	for _, version := range d.Spec.VersionData.Versions {
   103  		if len(version.ExtendedPaths.Virtual) > 0 {
   104  			return true
   105  		}
   106  	}
   107  	return false
   108  }
   109  
   110  func (d *VirtualEndpoint) getMetaFromRequest(r *http.Request) *apidef.VirtualMeta {
   111  	_, versionPaths, _, _ := d.Spec.Version(r)
   112  	found, meta := d.Spec.CheckSpecMatchesStatus(r, versionPaths, VirtualPath)
   113  	if !found {
   114  		return nil
   115  	}
   116  
   117  	vmeta, ok := meta.(*apidef.VirtualMeta)
   118  	if !ok {
   119  		return nil
   120  	}
   121  
   122  	return vmeta
   123  }
   124  
   125  func (d *VirtualEndpoint) ServeHTTPForCache(w http.ResponseWriter, r *http.Request, vmeta *apidef.VirtualMeta) *http.Response {
   126  	t1 := time.Now().UnixNano()
   127  
   128  	if vmeta == nil {
   129  		if vmeta = d.getMetaFromRequest(r); vmeta == nil {
   130  			return nil
   131  		}
   132  	}
   133  
   134  	// Create the proxy object
   135  	originalBody, err := ioutil.ReadAll(r.Body)
   136  	if err != nil {
   137  		d.Logger().WithError(err).Error("Failed to read request body!")
   138  		return nil
   139  	}
   140  	defer r.Body.Close()
   141  
   142  	scheme := "http"
   143  	if r.TLS != nil {
   144  		scheme = "https"
   145  	}
   146  	requestData := RequestObject{
   147  		Headers: r.Header,
   148  		Body:    string(originalBody),
   149  		URL:     r.URL.String(),
   150  		Scheme:  scheme,
   151  	}
   152  
   153  	// We need to copy the body _back_ for the decode
   154  	r.Body = ioutil.NopCloser(bytes.NewReader(originalBody))
   155  	parseForm(r)
   156  	requestData.Params = r.Form
   157  
   158  	requestAsJson, err := json.Marshal(requestData)
   159  	if err != nil {
   160  		d.Logger().WithError(err).Error("Failed to encode request object for virtual endpoint")
   161  		return nil
   162  	}
   163  
   164  	// Encode the configuration data too
   165  	specAsJson := specToJson(d.Spec)
   166  
   167  	session := new(user.SessionState)
   168  	session.Mutex = &sync.RWMutex{}
   169  
   170  	// Encode the session object (if not a pre-process)
   171  	if vmeta.UseSession {
   172  		session = ctxGetSession(r)
   173  	}
   174  
   175  	sessionAsJson, err := json.Marshal(session)
   176  	if err != nil {
   177  		d.Logger().WithError(err).Error("Failed to encode session for VM")
   178  		return nil
   179  	}
   180  
   181  	// Run the middleware
   182  	vm := d.Spec.JSVM.VM.Copy()
   183  	vm.Interrupt = make(chan func(), 1)
   184  	d.Logger().Debug("Running: ", vmeta.ResponseFunctionName)
   185  	// buffered, leaving no chance of a goroutine leak since the
   186  	// spawned goroutine will send 0 or 1 values.
   187  	ret := make(chan otto.Value, 1)
   188  	errRet := make(chan error, 1)
   189  	go func() {
   190  		defer func() {
   191  			// the VM executes the panic func that gets it
   192  			// to stop, so we must recover here to not crash
   193  			// the whole Go program.
   194  			recover()
   195  		}()
   196  		returnRaw, err := vm.Run(vmeta.ResponseFunctionName + `(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`)
   197  		ret <- returnRaw
   198  		errRet <- err
   199  	}()
   200  	var returnRaw otto.Value
   201  	t := time.NewTimer(d.Spec.JSVM.Timeout)
   202  	select {
   203  	case returnRaw = <-ret:
   204  		if err := <-errRet; err != nil {
   205  			d.Logger().WithError(err).Error("Failed to run JS middleware")
   206  			return nil
   207  		}
   208  		t.Stop()
   209  	case <-t.C:
   210  		t.Stop()
   211  		d.Logger().Error("JS middleware timed out after ", d.Spec.JSVM.Timeout)
   212  		vm.Interrupt <- func() {
   213  			// only way to stop the VM is to send it a func
   214  			// that panics.
   215  			panic("stop")
   216  		}
   217  		return nil
   218  	}
   219  	returnDataStr, _ := returnRaw.ToString()
   220  
   221  	// Decode the return object
   222  	newResponseData := VMResponseObject{}
   223  	if err := json.Unmarshal([]byte(returnDataStr), &newResponseData); err != nil {
   224  		d.Logger().WithError(err).Error("Failed to decode virtual endpoint response data on return from VM: ",
   225  			"; Returned: ", returnDataStr)
   226  		return nil
   227  	}
   228  
   229  	// Save the sesison data (if modified)
   230  	if vmeta.UseSession {
   231  		newMeta := mapStrsToIfaces(newResponseData.SessionMeta)
   232  		if !reflect.DeepEqual(session.GetMetaData(), newMeta) {
   233  			session.SetMetaData(newMeta)
   234  			ctxSetSession(r, session, "", true)
   235  		}
   236  	}
   237  
   238  	d.Logger().Debug("JSVM Virtual Endpoint execution took: (ns) ", time.Now().UnixNano()-t1)
   239  
   240  	copiedResponse := forceResponse(w, r, &newResponseData, d.Spec, session, false, d.Logger())
   241  
   242  	if copiedResponse != nil {
   243  		d.sh.RecordHit(r, Latency{}, copiedResponse.StatusCode, copiedResponse)
   244  	}
   245  
   246  	return copiedResponse
   247  }
   248  
   249  func forceResponse(w http.ResponseWriter,
   250  	r *http.Request,
   251  	newResponseData *VMResponseObject,
   252  	spec *APISpec,
   253  	session *user.SessionState, isPre bool, logger *logrus.Entry) *http.Response {
   254  	responseMessage := []byte(newResponseData.Response.Body)
   255  
   256  	// Create an http.Response object so we can send it tot he cache middleware
   257  	newResponse := new(http.Response)
   258  	newResponse.Header = make(map[string][]string)
   259  
   260  	requestTime := time.Now().UTC().Format(http.TimeFormat)
   261  
   262  	for header, value := range newResponseData.Response.Headers {
   263  		newResponse.Header.Set(header, value)
   264  	}
   265  
   266  	newResponse.ContentLength = int64(len(responseMessage))
   267  	newResponse.Body = nopCloser{
   268  		ReadSeeker: bytes.NewReader(responseMessage),
   269  	}
   270  	newResponse.StatusCode = newResponseData.Response.Code
   271  	newResponse.Proto = "HTTP/1.0"
   272  	newResponse.ProtoMajor = 1
   273  	newResponse.ProtoMinor = 0
   274  	newResponse.Header.Set("Server", "tyk")
   275  	newResponse.Header.Set("Date", requestTime)
   276  
   277  	// Check if it is a loop
   278  	loc := newResponse.Header.Get("Location")
   279  	if (newResponse.StatusCode == 301 || newResponse.StatusCode == 302) && strings.HasPrefix(loc, "tyk://") {
   280  		loopURL, err := url.Parse(newResponse.Header.Get("Location"))
   281  		if err != nil {
   282  			logger.WithError(err).WithField("loop", loc).Error("Failed to parse loop url")
   283  		} else {
   284  			ctxSetOrigRequestURL(r, r.URL)
   285  			r.URL = loopURL
   286  		}
   287  
   288  		return nil
   289  	}
   290  
   291  	if !isPre {
   292  		// Handle response middleware
   293  		if err := handleResponseChain(spec.ResponseChain, w, newResponse, r, session); err != nil {
   294  			logger.WithError(err).Error("Response chain failed! ")
   295  		}
   296  	}
   297  
   298  	handleForcedResponse(w, newResponse, session, spec)
   299  
   300  	// Record analytics
   301  	return newResponse
   302  }
   303  
   304  // ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
   305  func (d *VirtualEndpoint) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
   306  	vmeta := d.getMetaFromRequest(r)
   307  	if vmeta == nil {
   308  		// nothing can be done here, reply with 200 to allow proxy to target
   309  		return nil, http.StatusOK
   310  	}
   311  
   312  	if res := d.ServeHTTPForCache(w, r, vmeta); res == nil {
   313  		if vmeta.ProxyOnError {
   314  			return nil, http.StatusOK
   315  		} else {
   316  			return errors.New("Error during virtual endpoint execution. Contact Administrator for more details."), http.StatusInternalServerError
   317  		}
   318  	}
   319  
   320  	return nil, mwStatusRespond
   321  }
   322  
   323  func (d *VirtualEndpoint) HandleResponse(rw http.ResponseWriter, res *http.Response, ses *user.SessionState) {
   324  	// Externalising this from the MW so we can re-use it elsewhere
   325  	handleForcedResponse(rw, res, ses, d.Spec)
   326  }
   327  
   328  func handleForcedResponse(rw http.ResponseWriter, res *http.Response, ses *user.SessionState, spec *APISpec) {
   329  	defer res.Body.Close()
   330  
   331  	// Close connections
   332  	if spec.GlobalConfig.CloseConnections {
   333  		res.Header.Set("Connection", "close")
   334  	}
   335  
   336  	// Add resource headers
   337  	if ses != nil {
   338  		// We have found a session, lets report back
   339  		quotaMax, quotaRemaining, _, quotaRenews := ses.GetQuotaLimitByAPIID(spec.APIID)
   340  		res.Header.Set(headers.XRateLimitLimit, strconv.Itoa(int(quotaMax)))
   341  		res.Header.Set(headers.XRateLimitRemaining, strconv.Itoa(int(quotaRemaining)))
   342  		res.Header.Set(headers.XRateLimitReset, strconv.Itoa(int(quotaRenews)))
   343  	}
   344  
   345  	copyHeader(rw.Header(), res.Header)
   346  
   347  	rw.WriteHeader(res.StatusCode)
   348  	io.Copy(rw, res.Body)
   349  }