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  }