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 }