github.com/Finschia/finschia-sdk@v0.48.1/server/api/server.go (about)

     1  package api
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/http"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/gogo/gateway"
    12  	"github.com/gorilla/handlers"
    13  	"github.com/gorilla/mux"
    14  	"github.com/grpc-ecosystem/grpc-gateway/runtime"
    15  
    16  	"github.com/Finschia/ostracon/libs/log"
    17  	ostrpcserver "github.com/Finschia/ostracon/rpc/jsonrpc/server"
    18  
    19  	"github.com/Finschia/finschia-sdk/client"
    20  	"github.com/Finschia/finschia-sdk/codec/legacy"
    21  	"github.com/Finschia/finschia-sdk/server/config"
    22  	"github.com/Finschia/finschia-sdk/telemetry"
    23  	grpctypes "github.com/Finschia/finschia-sdk/types/grpc"
    24  
    25  	// unnamed import of statik for swagger UI support
    26  	_ "github.com/Finschia/finschia-sdk/client/docs/statik"
    27  )
    28  
    29  // Server defines the server's API interface.
    30  type Server struct {
    31  	Router            *mux.Router
    32  	GRPCGatewayRouter *runtime.ServeMux
    33  	ClientCtx         client.Context
    34  
    35  	logger  log.Logger
    36  	metrics *telemetry.Metrics
    37  	// Start() is blocking and generally called from a separate goroutine.
    38  	// Close() can be called asynchronously and access shared memory
    39  	// via the listener. Therefore, we sync access to Start and Close with
    40  	// this mutex to avoid data races.
    41  	mtx      sync.Mutex
    42  	listener net.Listener
    43  }
    44  
    45  // CustomGRPCHeaderMatcher for mapping request headers to
    46  // GRPC metadata.
    47  // HTTP headers that start with 'Grpc-Metadata-' are automatically mapped to
    48  // gRPC metadata after removing prefix 'Grpc-Metadata-'. We can use this
    49  // CustomGRPCHeaderMatcher if headers don't start with `Grpc-Metadata-`
    50  func CustomGRPCHeaderMatcher(key string) (string, bool) {
    51  	switch strings.ToLower(key) {
    52  	case grpctypes.GRPCBlockHeightHeader:
    53  		return grpctypes.GRPCBlockHeightHeader, true
    54  	default:
    55  		return runtime.DefaultHeaderMatcher(key)
    56  	}
    57  }
    58  
    59  func New(clientCtx client.Context, logger log.Logger) *Server {
    60  	// The default JSON marshaller used by the gRPC-Gateway is unable to marshal non-nullable non-scalar fields.
    61  	// Using the gogo/gateway package with the gRPC-Gateway WithMarshaler option fixes the scalar field marshalling issue.
    62  	marshalerOption := &gateway.JSONPb{
    63  		EmitDefaults: true,
    64  		Indent:       "  ",
    65  		OrigName:     true,
    66  		AnyResolver:  clientCtx.InterfaceRegistry,
    67  	}
    68  
    69  	return &Server{
    70  		Router:    mux.NewRouter(),
    71  		ClientCtx: clientCtx,
    72  		logger:    logger,
    73  		GRPCGatewayRouter: runtime.NewServeMux(
    74  			// Custom marshaler option is required for gogo proto
    75  			runtime.WithMarshalerOption(runtime.MIMEWildcard, marshalerOption),
    76  
    77  			// This is necessary to get error details properly
    78  			// marshalled in unary requests.
    79  			runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler),
    80  
    81  			// Custom header matcher for mapping request headers to
    82  			// GRPC metadata
    83  			runtime.WithIncomingHeaderMatcher(CustomGRPCHeaderMatcher),
    84  		),
    85  	}
    86  }
    87  
    88  // Start starts the API server. Internally, the API server leverages Tendermint's
    89  // JSON RPC server. Configuration options are provided via config.APIConfig
    90  // and are delegated to the Tendermint JSON RPC server. The process is
    91  // non-blocking, so an external signal handler must be used.
    92  func (s *Server) Start(cfg config.Config) error {
    93  	s.mtx.Lock()
    94  
    95  	ostCfg := ostrpcserver.DefaultConfig()
    96  	ostCfg.MaxOpenConnections = int(cfg.API.MaxOpenConnections)
    97  	ostCfg.ReadTimeout = time.Duration(cfg.API.RPCReadTimeout) * time.Second
    98  	ostCfg.WriteTimeout = time.Duration(cfg.API.RPCWriteTimeout) * time.Second
    99  	ostCfg.IdleTimeout = time.Duration(cfg.API.RPCIdleTimeout) * time.Second
   100  	ostCfg.MaxBodyBytes = int64(cfg.API.RPCMaxBodyBytes)
   101  
   102  	listener, err := ostrpcserver.Listen(cfg.API.Address, ostCfg)
   103  	if err != nil {
   104  		s.mtx.Unlock()
   105  		return err
   106  	}
   107  
   108  	s.registerGRPCGatewayRoutes()
   109  	s.listener = listener
   110  	var h http.Handler = s.Router
   111  
   112  	s.mtx.Unlock()
   113  
   114  	if cfg.API.EnableUnsafeCORS {
   115  		allowAllCORS := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))
   116  		return ostrpcserver.Serve(s.listener, allowAllCORS(h), s.logger, ostCfg)
   117  	}
   118  
   119  	s.logger.Info("starting API server...")
   120  	return ostrpcserver.Serve(s.listener, s.Router, s.logger, ostCfg)
   121  }
   122  
   123  // Close closes the API server.
   124  func (s *Server) Close() error {
   125  	s.mtx.Lock()
   126  	defer s.mtx.Unlock()
   127  	return s.listener.Close()
   128  }
   129  
   130  func (s *Server) registerGRPCGatewayRoutes() {
   131  	s.Router.PathPrefix("/").Handler(s.GRPCGatewayRouter)
   132  }
   133  
   134  func (s *Server) SetTelemetry(m *telemetry.Metrics) {
   135  	s.mtx.Lock()
   136  	s.metrics = m
   137  	s.registerMetrics()
   138  	s.mtx.Unlock()
   139  }
   140  
   141  func (s *Server) registerMetrics() {
   142  	metricsHandler := func(w http.ResponseWriter, r *http.Request) {
   143  		format := strings.TrimSpace(r.FormValue("format"))
   144  
   145  		gr, err := s.metrics.Gather(format)
   146  		if err != nil {
   147  			writeErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to gather metrics: %s", err))
   148  			return
   149  		}
   150  
   151  		w.Header().Set("Content-Type", gr.ContentType)
   152  		_, _ = w.Write(gr.Metrics)
   153  	}
   154  
   155  	s.Router.HandleFunc("/metrics", metricsHandler).Methods("GET")
   156  }
   157  
   158  // errorResponse defines the attributes of a JSON error response.
   159  type errorResponse struct {
   160  	Code  int    `json:"code,omitempty"`
   161  	Error string `json:"error"`
   162  }
   163  
   164  // newErrorResponse creates a new errorResponse instance.
   165  func newErrorResponse(code int, err string) errorResponse {
   166  	return errorResponse{Code: code, Error: err}
   167  }
   168  
   169  // writeErrorResponse prepares and writes a HTTP error
   170  // given a status code and an error message.
   171  func writeErrorResponse(w http.ResponseWriter, status int, err string) {
   172  	w.Header().Set("Content-Type", "application/json")
   173  	w.WriteHeader(status)
   174  	_, _ = w.Write(legacy.Cdc.MustMarshalJSON(newErrorResponse(0, err)))
   175  }