github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/mw_go_plugin.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "time" 9 10 "github.com/sirupsen/logrus" 11 12 "github.com/TykTechnologies/tyk/ctx" 13 "github.com/TykTechnologies/tyk/goplugin" 14 ) 15 16 // customResponseWriter is a wrapper around standard http.ResponseWriter 17 // plus it tracks if response was sent and what status code was sent 18 type customResponseWriter struct { 19 http.ResponseWriter 20 responseSent bool 21 statusCodeSent int 22 copyData bool 23 data []byte 24 dataLength int64 25 } 26 27 func (w *customResponseWriter) Write(b []byte) (int, error) { 28 w.responseSent = true 29 if w.statusCodeSent == 0 { 30 w.statusCodeSent = http.StatusOK // no WriteHeader was called so it will be set to StatusOK in actual ResponseWriter 31 } 32 33 // send actual data 34 num, err := w.ResponseWriter.Write(b) 35 36 // copy data sent 37 if w.copyData { 38 if w.data == nil { 39 w.data = make([]byte, num) 40 copy(w.data, b[:num]) 41 } else { 42 w.data = append(w.data, b[:num]...) 43 } 44 } 45 46 // count how many bytes we sent 47 w.dataLength += int64(num) 48 49 return num, err 50 } 51 52 func (w *customResponseWriter) WriteHeader(statusCode int) { 53 w.responseSent = true 54 w.statusCodeSent = statusCode 55 w.ResponseWriter.WriteHeader(statusCode) 56 } 57 58 func (w *customResponseWriter) getHttpResponse(r *http.Request) *http.Response { 59 // craft response on the fly for analytics 60 httpResponse := &http.Response{ 61 Status: http.StatusText(w.statusCodeSent), 62 StatusCode: w.statusCodeSent, 63 Header: w.ResponseWriter.Header(), // TODO: worth to think about trailer headers 64 Proto: r.Proto, 65 ProtoMajor: r.ProtoMajor, 66 ProtoMinor: r.ProtoMinor, 67 Request: r, 68 ContentLength: w.dataLength, 69 } 70 if w.copyData { 71 httpResponse.Body = ioutil.NopCloser(bytes.NewReader(w.data)) 72 } 73 74 return httpResponse 75 } 76 77 // GoPluginMiddleware is a generic middleware that will execute Go-plugin code before continuing 78 type GoPluginMiddleware struct { 79 BaseMiddleware 80 Path string // path to .so file 81 SymbolName string // function symbol to look up 82 handler http.HandlerFunc 83 logger *logrus.Entry 84 successHandler *SuccessHandler // to record analytics 85 } 86 87 func (m *GoPluginMiddleware) Name() string { 88 return "GoPluginMiddleware: " + m.Path + ":" + m.SymbolName 89 } 90 91 func (m *GoPluginMiddleware) EnabledForSpec() bool { 92 m.logger = log.WithFields(logrus.Fields{ 93 "mwPath": m.Path, 94 "mwSymbolName": m.SymbolName, 95 }) 96 97 if m.handler != nil { 98 m.logger.Info("Go-plugin middleware is already initialized") 99 return true 100 } 101 102 // try to load plugin 103 var err error 104 if m.handler, err = goplugin.GetHandler(m.Path, m.SymbolName); err != nil { 105 m.logger.WithError(err).Error("Could not load Go-plugin") 106 return false 107 } 108 109 // to record 2XX hits in analytics 110 m.successHandler = &SuccessHandler{BaseMiddleware: m.BaseMiddleware} 111 112 return true 113 } 114 115 func (m *GoPluginMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, conf interface{}) (err error, respCode int) { 116 // make sure tyk recover in case Go-plugin function panics 117 defer func() { 118 if e := recover(); e != nil { 119 err = fmt.Errorf("%v", e) 120 respCode = http.StatusInternalServerError 121 m.logger.WithError(err).Error("Recovered from panic while running Go-plugin middleware func") 122 } 123 }() 124 125 // prepare data to call Go-plugin function 126 127 // make sure request's body can be re-read again 128 nopCloseRequestBody(r) 129 130 // wrap ResponseWriter to check if response was sent 131 rw := &customResponseWriter{ 132 ResponseWriter: w, 133 copyData: recordDetail(r, m.Spec), 134 } 135 136 // call Go-plugin function 137 t1 := time.Now() 138 139 // Inject definition into request context: 140 ctx.SetDefinition(r, m.Spec.APIDefinition) 141 142 m.handler(rw, r) 143 144 // calculate latency 145 ms := DurationToMillisecond(time.Since(t1)) 146 m.logger.WithField("ms", ms).Debug("Go-plugin request processing took") 147 148 // check if response was sent 149 if rw.responseSent { 150 // check if response code was an error one 151 if rw.statusCodeSent >= http.StatusBadRequest { 152 // base middleware will report this error to analytics if needed 153 respCode = rw.statusCodeSent 154 err = fmt.Errorf("plugin function sent error response code: %d", rw.statusCodeSent) 155 m.logger.WithError(err).Error("Failed to process request with Go-plugin middleware func") 156 } else { 157 // record 2XX to analytics 158 m.successHandler.RecordHit(r, Latency{Total: int64(ms)}, rw.statusCodeSent, rw.getHttpResponse(r)) 159 160 // no need to continue passing this request down to reverse proxy 161 respCode = mwStatusRespond 162 } 163 } else { 164 respCode = http.StatusOK 165 } 166 167 return 168 }