github.com/cosmos/cosmos-sdk@v0.50.10/server/api/server.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	tmrpcserver "github.com/cometbft/cometbft/rpc/jsonrpc/server"
    13  	gateway "github.com/cosmos/gogogateway"
    14  	"github.com/gorilla/handlers"
    15  	"github.com/gorilla/mux"
    16  	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    17  	"github.com/improbable-eng/grpc-web/go/grpcweb"
    18  	"google.golang.org/grpc"
    19  
    20  	"cosmossdk.io/log"
    21  
    22  	"github.com/cosmos/cosmos-sdk/client"
    23  	"github.com/cosmos/cosmos-sdk/codec/legacy"
    24  	"github.com/cosmos/cosmos-sdk/server/config"
    25  	servercmtlog "github.com/cosmos/cosmos-sdk/server/log"
    26  	"github.com/cosmos/cosmos-sdk/telemetry"
    27  	grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
    28  )
    29  
    30  // Server defines the server's API interface.
    31  type Server struct {
    32  	Router            *mux.Router
    33  	GRPCGatewayRouter *runtime.ServeMux
    34  	ClientCtx         client.Context
    35  	GRPCSrv           *grpc.Server
    36  	logger            log.Logger
    37  	metrics           *telemetry.Metrics
    38  
    39  	// Start() is blocking and generally called from a separate goroutine.
    40  	// Close() can be called asynchronously and access shared memory
    41  	// via the listener. Therefore, we sync access to Start and Close with
    42  	// this mutex to avoid data races.
    43  	mtx      sync.Mutex
    44  	listener net.Listener
    45  }
    46  
    47  // CustomGRPCHeaderMatcher for mapping request headers to
    48  // GRPC metadata.
    49  // HTTP headers that start with 'Grpc-Metadata-' are automatically mapped to
    50  // gRPC metadata after removing prefix 'Grpc-Metadata-'. We can use this
    51  // CustomGRPCHeaderMatcher if headers don't start with `Grpc-Metadata-`
    52  func CustomGRPCHeaderMatcher(key string) (string, bool) {
    53  	switch strings.ToLower(key) {
    54  	case grpctypes.GRPCBlockHeightHeader:
    55  		return grpctypes.GRPCBlockHeightHeader, true
    56  
    57  	default:
    58  		return runtime.DefaultHeaderMatcher(key)
    59  	}
    60  }
    61  
    62  func New(clientCtx client.Context, logger log.Logger, grpcSrv *grpc.Server) *Server {
    63  	// The default JSON marshaller used by the gRPC-Gateway is unable to marshal non-nullable non-scalar fields.
    64  	// Using the gogo/gateway package with the gRPC-Gateway WithMarshaler option fixes the scalar field marshaling issue.
    65  	marshalerOption := &gateway.JSONPb{
    66  		EmitDefaults: true,
    67  		Indent:       "",
    68  		OrigName:     true,
    69  		AnyResolver:  clientCtx.InterfaceRegistry,
    70  	}
    71  
    72  	return &Server{
    73  		logger:    logger,
    74  		Router:    mux.NewRouter(),
    75  		ClientCtx: clientCtx,
    76  		GRPCGatewayRouter: runtime.NewServeMux(
    77  			// Custom marshaler option is required for gogo proto
    78  			runtime.WithMarshalerOption(runtime.MIMEWildcard, marshalerOption),
    79  
    80  			// This is necessary to get error details properly
    81  			// marshaled in unary requests.
    82  			runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),
    83  
    84  			// Custom header matcher for mapping request headers to
    85  			// GRPC metadata
    86  			runtime.WithIncomingHeaderMatcher(CustomGRPCHeaderMatcher),
    87  		),
    88  		GRPCSrv: grpcSrv,
    89  	}
    90  }
    91  
    92  // Start starts the API server. Internally, the API server leverages CometBFT's
    93  // JSON RPC server. Configuration options are provided via config.APIConfig
    94  // and are delegated to the CometBFT JSON RPC server.
    95  //
    96  // Note, this creates a blocking process if the server is started successfully.
    97  // Otherwise, an error is returned. The caller is expected to provide a Context
    98  // that is properly canceled or closed to indicate the server should be stopped.
    99  func (s *Server) Start(ctx context.Context, cfg config.Config) error {
   100  	s.mtx.Lock()
   101  
   102  	cmtCfg := tmrpcserver.DefaultConfig()
   103  	cmtCfg.MaxOpenConnections = int(cfg.API.MaxOpenConnections)
   104  	cmtCfg.ReadTimeout = time.Duration(cfg.API.RPCReadTimeout) * time.Second
   105  	cmtCfg.WriteTimeout = time.Duration(cfg.API.RPCWriteTimeout) * time.Second
   106  	cmtCfg.MaxBodyBytes = int64(cfg.API.RPCMaxBodyBytes)
   107  
   108  	listener, err := tmrpcserver.Listen(cfg.API.Address, cmtCfg.MaxOpenConnections)
   109  	if err != nil {
   110  		s.mtx.Unlock()
   111  		return err
   112  	}
   113  
   114  	s.listener = listener
   115  	s.mtx.Unlock()
   116  
   117  	// configure grpc-web server
   118  	if cfg.GRPC.Enable && cfg.GRPCWeb.Enable {
   119  		var options []grpcweb.Option
   120  		if cfg.API.EnableUnsafeCORS {
   121  			options = append(options,
   122  				grpcweb.WithOriginFunc(func(origin string) bool {
   123  					return true
   124  				}),
   125  			)
   126  		}
   127  
   128  		wrappedGrpc := grpcweb.WrapServer(s.GRPCSrv, options...)
   129  		s.Router.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   130  			if wrappedGrpc.IsGrpcWebRequest(req) {
   131  				wrappedGrpc.ServeHTTP(w, req)
   132  				return
   133  			}
   134  
   135  			// Fall back to grpc gateway server.
   136  			s.GRPCGatewayRouter.ServeHTTP(w, req)
   137  		}))
   138  	}
   139  
   140  	// register grpc-gateway routes (after grpc-web server as the first match is used)
   141  	s.Router.PathPrefix("/").Handler(s.GRPCGatewayRouter)
   142  
   143  	errCh := make(chan error)
   144  
   145  	// Start the API in an external goroutine as Serve is blocking and will return
   146  	// an error upon failure, which we'll send on the error channel that will be
   147  	// consumed by the for block below.
   148  	go func(enableUnsafeCORS bool) {
   149  		s.logger.Info("starting API server...", "address", cfg.API.Address)
   150  
   151  		if enableUnsafeCORS {
   152  			allowAllCORS := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))
   153  			errCh <- tmrpcserver.Serve(s.listener, allowAllCORS(s.Router), servercmtlog.CometLoggerWrapper{Logger: s.logger}, cmtCfg)
   154  		} else {
   155  			errCh <- tmrpcserver.Serve(s.listener, s.Router, servercmtlog.CometLoggerWrapper{Logger: s.logger}, cmtCfg)
   156  		}
   157  	}(cfg.API.EnableUnsafeCORS)
   158  
   159  	// Start a blocking select to wait for an indication to stop the server or that
   160  	// the server failed to start properly.
   161  	select {
   162  	case <-ctx.Done():
   163  		// The calling process canceled or closed the provided context, so we must
   164  		// gracefully stop the API server.
   165  		s.logger.Info("stopping API server...", "address", cfg.API.Address)
   166  		return s.Close()
   167  
   168  	case err := <-errCh:
   169  		s.logger.Error("failed to start API server", "err", err)
   170  		return err
   171  	}
   172  }
   173  
   174  // Close closes the API server.
   175  func (s *Server) Close() error {
   176  	s.mtx.Lock()
   177  	defer s.mtx.Unlock()
   178  	return s.listener.Close()
   179  }
   180  
   181  func (s *Server) SetTelemetry(m *telemetry.Metrics) {
   182  	s.mtx.Lock()
   183  	s.metrics = m
   184  	s.registerMetrics()
   185  	s.mtx.Unlock()
   186  }
   187  
   188  func (s *Server) registerMetrics() {
   189  	metricsHandler := func(w http.ResponseWriter, r *http.Request) {
   190  		format := strings.TrimSpace(r.FormValue("format"))
   191  
   192  		gr, err := s.metrics.Gather(format)
   193  		if err != nil {
   194  			writeErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to gather metrics: %s", err))
   195  			return
   196  		}
   197  
   198  		w.Header().Set("Content-Type", gr.ContentType)
   199  		_, _ = w.Write(gr.Metrics)
   200  	}
   201  
   202  	s.Router.HandleFunc("/metrics", metricsHandler).Methods("GET")
   203  }
   204  
   205  // errorResponse defines the attributes of a JSON error response.
   206  type errorResponse struct {
   207  	Code  int    `json:"code,omitempty"`
   208  	Error string `json:"error"`
   209  }
   210  
   211  // newErrorResponse creates a new errorResponse instance.
   212  func newErrorResponse(code int, err string) errorResponse {
   213  	return errorResponse{Code: code, Error: err}
   214  }
   215  
   216  // writeErrorResponse prepares and writes a HTTP error
   217  // given a status code and an error message.
   218  func writeErrorResponse(w http.ResponseWriter, status int, err string) {
   219  	w.Header().Set("Content-Type", "application/json")
   220  	w.WriteHeader(status)
   221  	_, _ = w.Write(legacy.Cdc.MustMarshalJSON(newErrorResponse(0, err)))
   222  }