github.com/viant/toolbox@v0.34.5/bridge/http_bridge.go (about)

     1  package bridge
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"github.com/viant/toolbox"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"net"
    13  	"net/http"
    14  	"net/http/httputil"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"sync"
    19  	"time"
    20  	"unicode"
    21  	"unicode/utf8"
    22  )
    23  
    24  //HttpBridgeConfig represent http bridge config
    25  type HttpBridgeEndpointConfig struct {
    26  	Port           string
    27  	ReadTimeoutMs  int
    28  	WriteTimeoutMs int
    29  	MaxHeaderBytes int
    30  }
    31  
    32  //HttpBridgeProxyRoute represent http proxy route
    33  type HttpBridgeProxyRoute struct {
    34  	Pattern          string
    35  	TargetURL        *url.URL
    36  	ResponseModifier func(*http.Response) error
    37  	Listener         func(request *http.Request, response *http.Response)
    38  }
    39  
    40  //HttpBridgeProxyConfig represent proxy config
    41  type HttpBridgeProxyConfig struct {
    42  	MaxIdleConnections    int
    43  	RequestTimeoutMs      int
    44  	KeepAliveTimeMs       int
    45  	TLSHandshakeTimeoutMs int
    46  	BufferPoolSize        int
    47  	BufferSize            int
    48  }
    49  
    50  //HttpBridgeConfig represents HttpBridgeConfig config
    51  type HttpBridgeConfig struct {
    52  	Endpoint *HttpBridgeEndpointConfig
    53  	Proxy    *HttpBridgeProxyConfig
    54  	Routes   []*HttpBridgeProxyRoute
    55  }
    56  
    57  //ProxyHandlerFactory proxy handler factory
    58  type HttpBridgeProxyHandlerFactory func(proxyConfig *HttpBridgeProxyConfig, route *HttpBridgeProxyRoute) (http.Handler, error)
    59  
    60  //HttpBridge represents http bridge
    61  type HttpBridge struct {
    62  	Config   *HttpBridgeConfig
    63  	Server   *http.Server
    64  	Handlers map[string]http.Handler
    65  }
    66  
    67  //ListenAndServe start http endpoint
    68  func (r *HttpBridge) ListenAndServe() error {
    69  	return r.Server.ListenAndServe()
    70  }
    71  
    72  //ListenAndServe start http endpoint on secure port
    73  func (r *HttpBridge) ListenAndServeTLS(certFile, keyFile string) error {
    74  	return r.Server.ListenAndServeTLS(certFile, keyFile)
    75  }
    76  
    77  //NewHttpBridge creates a new instance of NewHttpBridge
    78  func NewHttpBridge(config *HttpBridgeConfig, factory HttpBridgeProxyHandlerFactory) (*HttpBridge, error) {
    79  	mux := http.NewServeMux()
    80  	var handlers = make(map[string]http.Handler)
    81  	for _, route := range config.Routes {
    82  		handler, err := factory(config.Proxy, route)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		mux.Handle(route.Pattern, handler)
    87  		handlers[route.Pattern] = handler
    88  	}
    89  	server := &http.Server{
    90  		Addr:           ":" + config.Endpoint.Port,
    91  		Handler:        mux,
    92  		ReadTimeout:    time.Millisecond * time.Duration(config.Endpoint.ReadTimeoutMs),
    93  		WriteTimeout:   time.Millisecond * time.Duration(config.Endpoint.WriteTimeoutMs),
    94  		MaxHeaderBytes: config.Endpoint.MaxHeaderBytes,
    95  	}
    96  	return &HttpBridge{
    97  		Server:   server,
    98  		Config:   config,
    99  		Handlers: handlers,
   100  	}, nil
   101  }
   102  
   103  type handlerWrapper struct {
   104  	Handler http.Handler
   105  }
   106  
   107  func (h *handlerWrapper) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
   108  	h.Handler.ServeHTTP(writer, request)
   109  }
   110  
   111  //NewProxyHandler creates a new proxy handler
   112  func NewProxyHandler(proxyConfig *HttpBridgeProxyConfig, route *HttpBridgeProxyRoute) (http.Handler, error) {
   113  	roundTripper := &http.Transport{
   114  		Proxy: http.ProxyFromEnvironment,
   115  		Dial: (&net.Dialer{
   116  			Timeout:   time.Duration(proxyConfig.RequestTimeoutMs) * time.Millisecond,
   117  			KeepAlive: time.Duration(proxyConfig.KeepAliveTimeMs) * time.Millisecond,
   118  		}).Dial,
   119  		TLSHandshakeTimeout: time.Duration(proxyConfig.TLSHandshakeTimeoutMs) * time.Millisecond,
   120  		MaxIdleConnsPerHost: proxyConfig.MaxIdleConnections,
   121  	}
   122  
   123  	var director func(*http.Request)
   124  
   125  	if route.TargetURL != nil {
   126  		director = func(request *http.Request) {
   127  			request.URL.Scheme = route.TargetURL.Scheme
   128  			request.URL.Host = route.TargetURL.Host
   129  		}
   130  	}
   131  	reverseProxy := &httputil.ReverseProxy{
   132  		Transport:      roundTripper,
   133  		BufferPool:     toolbox.NewBytesBufferPool(proxyConfig.BufferPoolSize, proxyConfig.BufferSize),
   134  		ModifyResponse: route.ResponseModifier,
   135  		Director:       director,
   136  	}
   137  	var handler http.Handler = &handlerWrapper{reverseProxy}
   138  	return handler, nil
   139  }
   140  
   141  //HTTPTrip represents recorded round trip.
   142  type HttpTrip struct {
   143  	responseWriter     http.ResponseWriter
   144  	Request            *http.Request
   145  	responseBody       *bytes.Buffer
   146  	responseStatusCode int
   147  }
   148  
   149  func (w *HttpTrip) Response() *http.Response {
   150  	return &http.Response{
   151  		Request:    w.Request,
   152  		StatusCode: w.responseStatusCode,
   153  		Header:     w.responseWriter.Header(),
   154  		Body:       ioutil.NopCloser(bytes.NewReader(w.responseBody.Bytes())),
   155  	}
   156  }
   157  
   158  func (w *HttpTrip) Write(b []byte) (int, error) {
   159  	w.responseBody.Write(b)
   160  	return w.responseWriter.Write(b)
   161  }
   162  
   163  func (w *HttpTrip) Header() http.Header {
   164  	return w.responseWriter.Header()
   165  }
   166  
   167  func (w *HttpTrip) WriteHeader(status int) {
   168  	w.responseStatusCode = status
   169  	w.responseWriter.WriteHeader(status)
   170  }
   171  
   172  func (w *HttpTrip) Flush() {
   173  	if flusher, ok := w.responseWriter.(http.Flusher); ok {
   174  		flusher.Flush()
   175  	}
   176  }
   177  
   178  func (w *HttpTrip) CloseNotify() <-chan bool {
   179  	if closer, ok := w.responseWriter.(http.CloseNotifier); ok {
   180  		return closer.CloseNotify()
   181  	}
   182  	return make(chan bool, 1)
   183  }
   184  
   185  //ListeningTripHandler represents endpoint recording handler
   186  type ListeningTripHandler struct {
   187  	handler         http.Handler
   188  	pool            httputil.BufferPool
   189  	listener        func(request *http.Request, response *http.Response)
   190  	roundTripsMutex *sync.RWMutex
   191  }
   192  
   193  func (h *ListeningTripHandler) Notify(roundTrip *HttpTrip) {
   194  	if h.listener != nil {
   195  		h.listener(roundTrip.Request, roundTrip.Response())
   196  	}
   197  }
   198  
   199  //drainBody reads all of b to memory and then returns two equivalent (modified version from  httputil)
   200  func (h ListeningTripHandler) drainBody(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser, error) {
   201  	if reader == http.NoBody {
   202  		return http.NoBody, http.NoBody, nil
   203  	}
   204  	var buf = new(bytes.Buffer)
   205  	toolbox.CopyWithBufferPool(reader, buf, h.pool)
   206  	return ioutil.NopCloser(bytes.NewReader(buf.Bytes())), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
   207  }
   208  
   209  func (h ListeningTripHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
   210  	var err error
   211  	var originalRequest = request.WithContext(request.Context())
   212  	if request.ContentLength > 0 {
   213  		request.Body, originalRequest.Body, err = h.drainBody(request.Body)
   214  		if err != nil {
   215  			log.Printf("failed to serve request :%v due to %v\n", request, err)
   216  			return
   217  		}
   218  	}
   219  	var recordedRoundTrip = &HttpTrip{
   220  		responseWriter: responseWriter,
   221  		Request:        originalRequest,
   222  		responseBody:   new(bytes.Buffer),
   223  	}
   224  	responseWriter = http.ResponseWriter(recordedRoundTrip)
   225  	defer h.Notify(recordedRoundTrip)
   226  	h.handler.ServeHTTP(responseWriter, request)
   227  }
   228  
   229  func NewListeningHandler(handler http.Handler, bufferPoolSize, bufferSize int, listener func(request *http.Request, response *http.Response)) *ListeningTripHandler {
   230  	var result = &ListeningTripHandler{
   231  		handler:         handler,
   232  		listener:        listener,
   233  		pool:            toolbox.NewBytesBufferPool(bufferPoolSize, bufferSize),
   234  		roundTripsMutex: &sync.RWMutex{},
   235  	}
   236  	return result
   237  }
   238  
   239  func NewProxyRecordingHandler(proxyConfig *HttpBridgeProxyConfig, route *HttpBridgeProxyRoute) (http.Handler, error) {
   240  	handler, err := NewProxyHandler(proxyConfig, route)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	response := NewListeningHandler(handler, proxyConfig.BufferPoolSize, proxyConfig.BufferSize, route.Listener)
   245  	return response, nil
   246  }
   247  
   248  func AsListeningTripHandler(handler http.Handler) *ListeningTripHandler {
   249  	if result, ok := handler.(*ListeningTripHandler); ok {
   250  		return result
   251  	}
   252  	return nil
   253  }
   254  
   255  //HttpRequest represents JSON serializable http request
   256  type HttpRequest struct {
   257  	Method      string      `json:",omitempty"`
   258  	URL         string      `json:",omitempty"`
   259  	Header      http.Header `json:",omitempty"`
   260  	Body        string      `json:",omitempty"`
   261  	ThinkTimeMs int         `json:",omitempty"`
   262  }
   263  
   264  //NewHTTPRequest create a new instance of http request
   265  func NewHTTPRequest(method, url, body string, header http.Header) *HttpRequest {
   266  	return &HttpRequest{
   267  		Method: method,
   268  		URL:    url,
   269  		Body:   body,
   270  		Header: header,
   271  	}
   272  }
   273  
   274  //HttpResponse represents JSON serializable http response
   275  type HttpResponse struct {
   276  	Code   int
   277  	Header http.Header `json:",omitempty"`
   278  	Body   string      `json:",omitempty"`
   279  }
   280  
   281  func ReaderAsText(reader io.Reader) string {
   282  	if reader == nil {
   283  		return ""
   284  	}
   285  	body, err := ioutil.ReadAll(reader)
   286  	if err != nil {
   287  		log.Fatal(err)
   288  	}
   289  	if isBinary(body) {
   290  		buf := new(bytes.Buffer)
   291  		encoder := base64.NewEncoder(base64.StdEncoding, buf)
   292  		encoder.Write(body)
   293  		encoder.Close()
   294  		return fmt.Sprintf("base64:%v", string(buf.Bytes()))
   295  
   296  	} else if len(body) > 0 {
   297  		return fmt.Sprintf("text:%v", string(body))
   298  	}
   299  	return ""
   300  }
   301  
   302  func isBinary(input []byte) bool {
   303  	for i, w := 0, 0; i < len(input); i += w {
   304  		runeValue, width := utf8.DecodeRune(input[i:])
   305  		if unicode.IsControl(runeValue) {
   306  			return true
   307  		}
   308  		w = width
   309  	}
   310  	return false
   311  }
   312  
   313  func writeData(filename string, source interface{}, printStrOut bool) error {
   314  	if toolbox.FileExists(filename) {
   315  		os.Remove(filename)
   316  	}
   317  
   318  	logfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
   319  	if err != nil {
   320  		log.Fatalf("error opening file: %v, %v", err, filename)
   321  	}
   322  	defer logfile.Close()
   323  
   324  	buf, err := json.MarshalIndent(source, "", "\t")
   325  	if err != nil {
   326  		return err
   327  	}
   328  	_, err = logfile.Write(buf)
   329  
   330  	if printStrOut {
   331  		fmt.Printf("%v: %v\n", filename, string(buf))
   332  	}
   333  
   334  	return err
   335  }
   336  
   337  //HttpFileRecorder returns http route listener that will record request response to the passed in directory
   338  func HttpFileRecorder(directory string, printStdOut bool) func(request *http.Request, response *http.Response) {
   339  	tripCounter := 0
   340  
   341  	err := toolbox.CreateDirIfNotExist(directory)
   342  	if err != nil {
   343  		fmt.Printf("failed to create directory%v %v\n, ", err, directory)
   344  	}
   345  	return func(request *http.Request, response *http.Response) {
   346  		var body string
   347  		if request.Body != nil {
   348  			body = ReaderAsText(request.Body)
   349  		}
   350  		httpRequest := &HttpRequest{
   351  			Method: request.Method,
   352  			URL:    request.URL.String(),
   353  			Header: request.Header,
   354  			Body:   body,
   355  		}
   356  
   357  		err = writeData(path.Join(directory, fmt.Sprintf("%T-%v.json", *httpRequest, tripCounter)), httpRequest, printStdOut)
   358  		if err != nil {
   359  			fmt.Printf("failed to write request %v %v\n, ", err, request)
   360  		}
   361  
   362  		body = ReaderAsText(response.Body)
   363  		request.Body = nil
   364  		httpResponse := &HttpResponse{
   365  			Code:   response.StatusCode,
   366  			Header: response.Header,
   367  			Body:   body,
   368  		}
   369  
   370  		err = writeData(path.Join(directory, fmt.Sprintf("%T-%v.json", *httpResponse, tripCounter)), httpResponse, printStdOut)
   371  		if err != nil {
   372  			fmt.Printf("failed to write response %v %v\n, ", err, response)
   373  		}
   374  
   375  		tripCounter++
   376  	}
   377  
   378  }