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 }