github.com/MetalBlockchain/metalgo@v1.11.9/api/server/server.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package server 5 6 import ( 7 "context" 8 "fmt" 9 "net" 10 "net/http" 11 "net/url" 12 "path" 13 "time" 14 15 "github.com/NYTimes/gziphandler" 16 "github.com/prometheus/client_golang/prometheus" 17 "github.com/rs/cors" 18 "go.uber.org/zap" 19 "golang.org/x/net/http2" 20 21 "github.com/MetalBlockchain/metalgo/api" 22 "github.com/MetalBlockchain/metalgo/ids" 23 "github.com/MetalBlockchain/metalgo/snow" 24 "github.com/MetalBlockchain/metalgo/snow/engine/common" 25 "github.com/MetalBlockchain/metalgo/trace" 26 "github.com/MetalBlockchain/metalgo/utils/constants" 27 "github.com/MetalBlockchain/metalgo/utils/logging" 28 ) 29 30 const ( 31 baseURL = "/ext" 32 maxConcurrentStreams = 64 33 ) 34 35 var ( 36 _ PathAdder = readPathAdder{} 37 _ Server = (*server)(nil) 38 ) 39 40 type PathAdder interface { 41 // AddRoute registers a route to a handler. 42 AddRoute(handler http.Handler, base, endpoint string) error 43 44 // AddAliases registers aliases to the server 45 AddAliases(endpoint string, aliases ...string) error 46 } 47 48 type PathAdderWithReadLock interface { 49 // AddRouteWithReadLock registers a route to a handler assuming the http 50 // read lock is currently held. 51 AddRouteWithReadLock(handler http.Handler, base, endpoint string) error 52 53 // AddAliasesWithReadLock registers aliases to the server assuming the http read 54 // lock is currently held. 55 AddAliasesWithReadLock(endpoint string, aliases ...string) error 56 } 57 58 // Server maintains the HTTP router 59 type Server interface { 60 PathAdder 61 PathAdderWithReadLock 62 // Dispatch starts the API server 63 Dispatch() error 64 // RegisterChain registers the API endpoints associated with this chain. 65 // That is, add <route, handler> pairs to server so that API calls can be 66 // made to the VM. 67 RegisterChain(chainName string, ctx *snow.ConsensusContext, vm common.VM) 68 // Shutdown this server 69 Shutdown() error 70 } 71 72 type HTTPConfig struct { 73 ReadTimeout time.Duration `json:"readTimeout"` 74 ReadHeaderTimeout time.Duration `json:"readHeaderTimeout"` 75 WriteTimeout time.Duration `json:"writeHeaderTimeout"` 76 IdleTimeout time.Duration `json:"idleTimeout"` 77 } 78 79 type server struct { 80 // log this server writes to 81 log logging.Logger 82 // generates new logs for chains to write to 83 factory logging.Factory 84 85 shutdownTimeout time.Duration 86 87 tracingEnabled bool 88 tracer trace.Tracer 89 90 metrics *metrics 91 92 // Maps endpoints to handlers 93 router *router 94 95 srv *http.Server 96 97 // Listener used to serve traffic 98 listener net.Listener 99 } 100 101 // New returns an instance of a Server. 102 func New( 103 log logging.Logger, 104 factory logging.Factory, 105 listener net.Listener, 106 allowedOrigins []string, 107 shutdownTimeout time.Duration, 108 nodeID ids.NodeID, 109 tracingEnabled bool, 110 tracer trace.Tracer, 111 registerer prometheus.Registerer, 112 httpConfig HTTPConfig, 113 allowedHosts []string, 114 ) (Server, error) { 115 m, err := newMetrics(registerer) 116 if err != nil { 117 return nil, err 118 } 119 120 router := newRouter() 121 allowedHostsHandler := filterInvalidHosts(router, allowedHosts) 122 corsHandler := cors.New(cors.Options{ 123 AllowedOrigins: allowedOrigins, 124 AllowCredentials: true, 125 }).Handler(allowedHostsHandler) 126 gzipHandler := gziphandler.GzipHandler(corsHandler) 127 var handler http.Handler = http.HandlerFunc( 128 func(w http.ResponseWriter, r *http.Request) { 129 // Attach this node's ID as a header 130 w.Header().Set("node-id", nodeID.String()) 131 gzipHandler.ServeHTTP(w, r) 132 }, 133 ) 134 135 httpServer := &http.Server{ 136 Handler: handler, 137 ReadTimeout: httpConfig.ReadTimeout, 138 ReadHeaderTimeout: httpConfig.ReadHeaderTimeout, 139 WriteTimeout: httpConfig.WriteTimeout, 140 IdleTimeout: httpConfig.IdleTimeout, 141 } 142 err = http2.ConfigureServer(httpServer, &http2.Server{ 143 MaxConcurrentStreams: maxConcurrentStreams, 144 }) 145 if err != nil { 146 return nil, err 147 } 148 149 log.Info("API created", 150 zap.Strings("allowedOrigins", allowedOrigins), 151 ) 152 153 return &server{ 154 log: log, 155 factory: factory, 156 shutdownTimeout: shutdownTimeout, 157 tracingEnabled: tracingEnabled, 158 tracer: tracer, 159 metrics: m, 160 router: router, 161 srv: httpServer, 162 listener: listener, 163 }, nil 164 } 165 166 func (s *server) Dispatch() error { 167 return s.srv.Serve(s.listener) 168 } 169 170 func (s *server) RegisterChain(chainName string, ctx *snow.ConsensusContext, vm common.VM) { 171 ctx.Lock.Lock() 172 handlers, err := vm.CreateHandlers(context.TODO()) 173 ctx.Lock.Unlock() 174 if err != nil { 175 s.log.Error("failed to create handlers", 176 zap.String("chainName", chainName), 177 zap.Error(err), 178 ) 179 return 180 } 181 182 s.log.Verbo("about to add API endpoints", 183 zap.Stringer("chainID", ctx.ChainID), 184 ) 185 // all subroutes to a chain begin with "bc/<the chain's ID>" 186 defaultEndpoint := path.Join(constants.ChainAliasPrefix, ctx.ChainID.String()) 187 188 // Register each endpoint 189 for extension, handler := range handlers { 190 // Validate that the route being added is valid 191 // e.g. "/foo" and "" are ok but "\n" is not 192 _, err := url.ParseRequestURI(extension) 193 if extension != "" && err != nil { 194 s.log.Error("could not add route to chain's API handler", 195 zap.String("reason", "route is malformed"), 196 zap.Error(err), 197 ) 198 continue 199 } 200 if err := s.addChainRoute(chainName, handler, ctx, defaultEndpoint, extension); err != nil { 201 s.log.Error("error adding route", 202 zap.Error(err), 203 ) 204 } 205 } 206 } 207 208 func (s *server) addChainRoute(chainName string, handler http.Handler, ctx *snow.ConsensusContext, base, endpoint string) error { 209 url := fmt.Sprintf("%s/%s", baseURL, base) 210 s.log.Info("adding route", 211 zap.String("url", url), 212 zap.String("endpoint", endpoint), 213 ) 214 if s.tracingEnabled { 215 handler = api.TraceHandler(handler, chainName, s.tracer) 216 } 217 // Apply middleware to reject calls to the handler before the chain finishes bootstrapping 218 handler = rejectMiddleware(handler, ctx) 219 handler = s.metrics.wrapHandler(chainName, handler) 220 return s.router.AddRouter(url, endpoint, handler) 221 } 222 223 func (s *server) AddRoute(handler http.Handler, base, endpoint string) error { 224 return s.addRoute(handler, base, endpoint) 225 } 226 227 func (s *server) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error { 228 s.router.lock.RUnlock() 229 defer s.router.lock.RLock() 230 return s.addRoute(handler, base, endpoint) 231 } 232 233 func (s *server) addRoute(handler http.Handler, base, endpoint string) error { 234 url := fmt.Sprintf("%s/%s", baseURL, base) 235 s.log.Info("adding route", 236 zap.String("url", url), 237 zap.String("endpoint", endpoint), 238 ) 239 240 if s.tracingEnabled { 241 handler = api.TraceHandler(handler, url, s.tracer) 242 } 243 244 handler = s.metrics.wrapHandler(base, handler) 245 return s.router.AddRouter(url, endpoint, handler) 246 } 247 248 // Reject middleware wraps a handler. If the chain that the context describes is 249 // not done state-syncing/bootstrapping, writes back an error. 250 func rejectMiddleware(handler http.Handler, ctx *snow.ConsensusContext) http.Handler { 251 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // If chain isn't done bootstrapping, ignore API calls 252 if ctx.State.Get().State != snow.NormalOp { 253 http.Error(w, "API call rejected because chain is not done bootstrapping", http.StatusServiceUnavailable) 254 } else { 255 handler.ServeHTTP(w, r) 256 } 257 }) 258 } 259 260 func (s *server) AddAliases(endpoint string, aliases ...string) error { 261 url := fmt.Sprintf("%s/%s", baseURL, endpoint) 262 endpoints := make([]string, len(aliases)) 263 for i, alias := range aliases { 264 endpoints[i] = fmt.Sprintf("%s/%s", baseURL, alias) 265 } 266 return s.router.AddAlias(url, endpoints...) 267 } 268 269 func (s *server) AddAliasesWithReadLock(endpoint string, aliases ...string) error { 270 // This is safe, as the read lock doesn't actually need to be held once the 271 // http handler is called. However, it is unlocked later, so this function 272 // must end with the lock held. 273 s.router.lock.RUnlock() 274 defer s.router.lock.RLock() 275 276 return s.AddAliases(endpoint, aliases...) 277 } 278 279 func (s *server) Shutdown() error { 280 ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) 281 err := s.srv.Shutdown(ctx) 282 cancel() 283 284 // If shutdown times out, make sure the server is still shutdown. 285 _ = s.srv.Close() 286 return err 287 } 288 289 type readPathAdder struct { 290 pather PathAdderWithReadLock 291 } 292 293 func PathWriterFromWithReadLock(pather PathAdderWithReadLock) PathAdder { 294 return readPathAdder{ 295 pather: pather, 296 } 297 } 298 299 func (a readPathAdder) AddRoute(handler http.Handler, base, endpoint string) error { 300 return a.pather.AddRouteWithReadLock(handler, base, endpoint) 301 } 302 303 func (a readPathAdder) AddAliases(endpoint string, aliases ...string) error { 304 return a.pather.AddAliasesWithReadLock(endpoint, aliases...) 305 }