code.vegaprotocol.io/vega@v0.79.0/datanode/gateway/rest/server.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package rest
    17  
    18  import (
    19  	"context"
    20  	"net"
    21  	"net/http"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"code.vegaprotocol.io/vega/datanode/gateway"
    26  	"code.vegaprotocol.io/vega/logging"
    27  	"code.vegaprotocol.io/vega/paths"
    28  	protoapiv2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    29  	vegaprotoapi "code.vegaprotocol.io/vega/protos/vega/api/v1"
    30  
    31  	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    32  	"github.com/tmc/grpc-websocket-proxy/wsproxy"
    33  	"go.elastic.co/apm/module/apmhttp"
    34  	"google.golang.org/grpc"
    35  	"google.golang.org/protobuf/encoding/protojson"
    36  )
    37  
    38  const (
    39  	namedLogger = "restproxy"
    40  )
    41  
    42  // ProxyServer implement a rest server acting as a proxy to the grpc api.
    43  type ProxyServer struct {
    44  	gateway.Config
    45  	log       *logging.Logger
    46  	vegaPaths paths.Paths
    47  	srv       *http.Server
    48  }
    49  
    50  // NewProxyServer returns a new instance of the rest proxy server.
    51  func NewProxyServer(log *logging.Logger, config gateway.Config, vegaPaths paths.Paths) *ProxyServer {
    52  	// setup logger
    53  	log = log.Named(namedLogger)
    54  	log.SetLevel(config.Level.Get())
    55  
    56  	return &ProxyServer{
    57  		log:       log,
    58  		Config:    config,
    59  		srv:       nil,
    60  		vegaPaths: vegaPaths,
    61  	}
    62  }
    63  
    64  // ReloadConf update the internal configuration of the server.
    65  func (s *ProxyServer) ReloadConf(cfg gateway.Config) {
    66  	s.log.Info("reloading configuration")
    67  	if s.log.GetLevel() != cfg.Level.Get() {
    68  		s.log.Info("updating log level",
    69  			logging.String("old", s.log.GetLevel().String()),
    70  			logging.String("new", cfg.Level.String()),
    71  		)
    72  		s.log.SetLevel(cfg.Level.Get())
    73  	}
    74  
    75  	// TODO(): not updating the the actual server for now, may need to look at this later
    76  	// e.g restart the http server on another port or whatever
    77  	s.Config = cfg
    78  }
    79  
    80  // This is because by default the marshaller wants to put a newline between chunks of the stream response.
    81  type HTTPBodyDelimitedMarshaler struct {
    82  	runtime.HTTPBodyMarshaler
    83  
    84  	delimiter []byte
    85  }
    86  
    87  func (o *HTTPBodyDelimitedMarshaler) Delimiter() []byte {
    88  	return o.delimiter
    89  }
    90  
    91  // Start start the server.
    92  func (s *ProxyServer) Start(ctx context.Context) (http.Handler, error) {
    93  	logger := s.log
    94  
    95  	grpcAddr := net.JoinHostPort(s.Node.IP, strconv.Itoa(s.Node.Port))
    96  
    97  	mux := runtime.NewServeMux(
    98  		// this is a settings specially made for websockets
    99  		runtime.WithMarshalerOption("application/json+stream", &JSONPb{
   100  			EnumsAsInts:  true,
   101  			EmitDefaults: false,
   102  			OrigName:     false,
   103  		}),
   104  		// prettified, just for JonRay
   105  		// append ?pretty to any query to make it... pretty
   106  		runtime.WithMarshalerOption("application/json+pretty", &JSONPb{
   107  			EnumsAsInts:  false,
   108  			EmitDefaults: true,
   109  			OrigName:     false,
   110  			Indent:       " ",
   111  		}),
   112  		runtime.WithMarshalerOption("text/csv", &HTTPBodyDelimitedMarshaler{
   113  			delimiter: []byte(""), // Don't append newline between stream sends
   114  			// Default HTTPBodyMarshaler
   115  			HTTPBodyMarshaler: runtime.HTTPBodyMarshaler{
   116  				Marshaler: &runtime.JSONPb{
   117  					MarshalOptions: protojson.MarshalOptions{
   118  						EmitUnpopulated: true,
   119  					},
   120  					UnmarshalOptions: protojson.UnmarshalOptions{
   121  						DiscardUnknown: true,
   122  					},
   123  				},
   124  			},
   125  		}),
   126  		// default for REST request
   127  		runtime.WithMarshalerOption(runtime.MIMEWildcard, &JSONPb{
   128  			EmitDefaults: true,
   129  			OrigName:     false,
   130  		}),
   131  
   132  		runtime.WithOutgoingHeaderMatcher(func(s string) (string, bool) { return s, true }),
   133  	)
   134  
   135  	opts := []grpc.DialOption{
   136  		grpc.WithInsecure(),
   137  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(s.Config.Node.MaxMsgSize)),
   138  	}
   139  
   140  	marshalW := func(h http.Handler) http.Handler {
   141  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   142  			// if we are dealing with a stream, let's add some header to use the proper marshaller
   143  			if strings.HasPrefix(r.URL.Path, "/api/v2/stream/") {
   144  				r.Header.Set("Accept", "application/json+stream")
   145  			} else if strings.HasPrefix(r.URL.Path, "/api/v2/networkhistory/export") ||
   146  				strings.HasPrefix(r.URL.Path, "/api/v2/ledgerentry/export") {
   147  				r.Header.Set("Accept", "text/csv")
   148  			} else if _, ok := r.URL.Query()["pretty"]; ok {
   149  				// checking Values as map[string][]string also catches ?pretty and ?pretty=
   150  				// r.URL.Query().Get("pretty") would not.
   151  				r.Header.Set("Accept", "application/json+pretty")
   152  			}
   153  			h.ServeHTTP(w, r)
   154  		})
   155  	}
   156  
   157  	if err := vegaprotoapi.RegisterCoreServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts); err != nil {
   158  		logger.Panic("Failure registering trading handler for REST proxy endpoints", logging.Error(err))
   159  	}
   160  	if err := protoapiv2.RegisterTradingDataServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts); err != nil {
   161  		logger.Panic("Failure registering trading handler for REST proxy endpoints", logging.Error(err))
   162  	}
   163  
   164  	handler := marshalW(mux)
   165  	handler = healthCheckMiddleware(handler)
   166  	handler = gateway.RemoteAddrMiddleware(logger, handler)
   167  	// Gzip encoding support
   168  	handler = NewGzipHandler(*logger, handler.(http.HandlerFunc))
   169  	// Metric support
   170  	handler = gateway.MetricCollectionMiddleware(handler)
   171  	handler = wsproxy.WebsocketProxy(handler)
   172  
   173  	// APM
   174  	if s.REST.APMEnabled {
   175  		handler = apmhttp.Wrap(handler)
   176  	}
   177  
   178  	return handler, nil
   179  }
   180  
   181  func healthCheckMiddleware(f http.Handler) http.HandlerFunc {
   182  	return func(w http.ResponseWriter, r *http.Request) {
   183  		if r.URL.Path == "/health" {
   184  			_, _ = w.Write([]byte("ok"))
   185  			w.WriteHeader(http.StatusOK)
   186  			return
   187  		}
   188  		f.ServeHTTP(w, r)
   189  	}
   190  }