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  }