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