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 }