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 }