mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
345 lines
9.5 KiB
Go
345 lines
9.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package server
|
|
|
|
import (
|
|
"github.com/luxfi/metric"
|
|
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
|
|
// "net/url" // Unused after handler registration moved to chain manager
|
|
// "path" // Unused after handler registration moved to chain manager
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/runtime"
|
|
"github.com/luxfi/trace"
|
|
"github.com/luxfi/vm"
|
|
"github.com/luxfi/vm/api"
|
|
"github.com/rs/cors"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/h2c"
|
|
|
|
// "github.com/luxfi/constants" // Unused after handler registration moved
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
const (
|
|
baseURL = "/ext"
|
|
maxConcurrentStreams = 64
|
|
)
|
|
|
|
var (
|
|
_ PathAdder = readPathAdder{}
|
|
_ Server = (*server)(nil)
|
|
)
|
|
|
|
type PathAdder interface {
|
|
// AddRoute registers a route to a handler.
|
|
AddRoute(handler http.Handler, base, endpoint string) error
|
|
|
|
// AddAliases registers aliases to the server
|
|
AddAliases(endpoint string, aliases ...string) error
|
|
}
|
|
|
|
type PathAdderWithReadLock interface {
|
|
// AddRouteWithReadLock registers a route to a handler assuming the http
|
|
// read lock is currently held.
|
|
AddRouteWithReadLock(handler http.Handler, base, endpoint string) error
|
|
|
|
// AddAliasesWithReadLock registers aliases to the server assuming the http read
|
|
// lock is currently held.
|
|
AddAliasesWithReadLock(endpoint string, aliases ...string) error
|
|
}
|
|
|
|
// Server maintains the HTTP router
|
|
type Server interface {
|
|
PathAdder
|
|
PathAdderWithReadLock
|
|
// Dispatch starts the API server
|
|
Dispatch() error
|
|
// RegisterChain registers the API endpoints associated with this chain.
|
|
// That is, add <route, handler> pairs to server so that API calls can be
|
|
// made to the VM.
|
|
RegisterChain(chainName string, ctx *runtime.Runtime, vm vm.VM)
|
|
// Shutdown this server
|
|
Shutdown() error
|
|
}
|
|
|
|
type HTTPConfig struct {
|
|
ReadTimeout time.Duration `json:"readTimeout"`
|
|
ReadHeaderTimeout time.Duration `json:"readHeaderTimeout"`
|
|
WriteTimeout time.Duration `json:"writeHeaderTimeout"`
|
|
IdleTimeout time.Duration `json:"idleTimeout"`
|
|
}
|
|
|
|
type server struct {
|
|
// log this server writes to
|
|
log log.Logger
|
|
|
|
shutdownTimeout time.Duration
|
|
|
|
tracingEnabled bool
|
|
tracer trace.Tracer
|
|
|
|
metrics *serverMetrics
|
|
|
|
// Maps endpoints to handlers
|
|
router *router
|
|
|
|
// Mutex for thread-safe operations
|
|
lock sync.RWMutex
|
|
|
|
srv *http.Server
|
|
|
|
// Listener used to serve traffic
|
|
listener net.Listener
|
|
}
|
|
|
|
// New returns an instance of a Server.
|
|
func New(
|
|
log log.Logger,
|
|
listener net.Listener,
|
|
allowedOrigins []string,
|
|
shutdownTimeout time.Duration,
|
|
nodeID ids.NodeID,
|
|
tracingEnabled bool,
|
|
tracer trace.Tracer,
|
|
registerer metric.Registerer,
|
|
httpConfig HTTPConfig,
|
|
allowedHosts []string,
|
|
) (Server, error) {
|
|
m, err := newMetrics(registerer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
router := newRouter()
|
|
handler := wrapHandler(router, nodeID, allowedOrigins, allowedHosts)
|
|
|
|
httpServer := &http.Server{
|
|
Handler: h2c.NewHandler(
|
|
handler,
|
|
&http2.Server{
|
|
MaxConcurrentStreams: maxConcurrentStreams,
|
|
}),
|
|
ReadTimeout: httpConfig.ReadTimeout,
|
|
ReadHeaderTimeout: httpConfig.ReadHeaderTimeout,
|
|
WriteTimeout: httpConfig.WriteTimeout,
|
|
IdleTimeout: httpConfig.IdleTimeout,
|
|
}
|
|
|
|
log.Info("API created with allowed origins: " + strings.Join(allowedOrigins, ","))
|
|
|
|
return &server{
|
|
log: log,
|
|
shutdownTimeout: shutdownTimeout,
|
|
tracingEnabled: tracingEnabled,
|
|
tracer: tracer,
|
|
metrics: m,
|
|
router: router,
|
|
srv: httpServer,
|
|
listener: listener,
|
|
}, nil
|
|
}
|
|
|
|
func (s *server) Dispatch() error {
|
|
return s.srv.Serve(s.listener)
|
|
}
|
|
|
|
func (s *server) RegisterChain(chainName string, ctx *runtime.Runtime, vm vm.VM) {
|
|
// Note: HTTP handler registration is now done in chains/manager.go:createChain()
|
|
// after VM initialization. This RegisterChain method is called too early (before
|
|
// VM initialization) and would cause nil pointer dereference if we call CreateHandlers here.
|
|
// The chain manager will call CreateHandlers at the appropriate time after notifyRegistrants.
|
|
|
|
s.log.Debug("chain registered (handlers will be created by chain manager)",
|
|
log.String("chainName", chainName),
|
|
log.Stringer("chainID", ctx.ChainID),
|
|
)
|
|
|
|
// DISABLED: Handler registration moved to chains/manager.go:createChain()
|
|
// all subroutes to a chain begin with "bc/<the chain's ID>"
|
|
// defaultEndpoint := path.Join(constants.ChainAliasPrefix, ctx.ChainID.String())
|
|
|
|
// DISABLED: Register each endpoint
|
|
/*
|
|
for extension, handler := range pathRouteHandlers {
|
|
// Validate that the route being added is valid
|
|
// e.g. "/foo" and "" are ok but "\n" is not
|
|
_, err := url.ParseRequestURI(extension)
|
|
if extension != "" && err != nil {
|
|
s.log.Error("could not add route to chain's API handler",
|
|
log.UserString("reason", "route is malformed"),
|
|
log.Err(err),
|
|
)
|
|
continue
|
|
}
|
|
if err := s.addChainRoute(chainName, handler, ctx, defaultEndpoint, extension); err != nil {
|
|
s.log.Error("error adding route",
|
|
log.Err(err),
|
|
)
|
|
}
|
|
}
|
|
|
|
ctx.Lock.Lock()
|
|
headerRouteHandler, err := vm.NewHTTPHandler(context.TODO())
|
|
ctx.Lock.Unlock()
|
|
if err != nil {
|
|
s.log.Error("failed to create header route handler",
|
|
log.String("chainName", chainName),
|
|
log.Err(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
if headerRouteHandler == nil {
|
|
return
|
|
}
|
|
|
|
headerRouteHandler = s.wrapMiddleware(chainName, headerRouteHandler, ctx)
|
|
if !s.router.AddHeaderRoute(ctx.ChainID.String(), headerRouteHandler) {
|
|
s.log.Error(
|
|
"failed to add header route",
|
|
log.String("chainName", chainName),
|
|
)
|
|
}
|
|
*/
|
|
}
|
|
|
|
func (s *server) addChainRoute(chainName string, handler http.Handler, ctx *runtime.Runtime, base, endpoint string) error {
|
|
url := fmt.Sprintf("%s/%s", baseURL, base)
|
|
s.log.Info("adding route",
|
|
log.UserString("url", url),
|
|
log.UserString("endpoint", endpoint),
|
|
)
|
|
handler = s.wrapMiddleware(chainName, handler, ctx)
|
|
return s.router.AddRouter(url, endpoint, handler)
|
|
}
|
|
|
|
func (s *server) wrapMiddleware(chainName string, handler http.Handler, ctx *runtime.Runtime) http.Handler {
|
|
if s.tracingEnabled {
|
|
handler = api.TraceHandler(handler, chainName, s.tracer)
|
|
}
|
|
// Apply middleware to reject calls to the handler before the chain finishes bootstrapping
|
|
handler = rejectMiddleware(handler, ctx)
|
|
return s.metrics.wrapHandler(chainName, handler)
|
|
}
|
|
|
|
func (s *server) AddRoute(handler http.Handler, base, endpoint string) error {
|
|
return s.addRoute(handler, base, endpoint)
|
|
}
|
|
|
|
func (s *server) AddRouteWithReadLock(handler http.Handler, base, endpoint string) error {
|
|
s.router.lock.RUnlock()
|
|
defer s.router.lock.RLock()
|
|
return s.addRoute(handler, base, endpoint)
|
|
}
|
|
|
|
func (s *server) addRoute(handler http.Handler, base, endpoint string) error {
|
|
url := fmt.Sprintf("%s/%s", baseURL, base)
|
|
s.log.Info("adding route",
|
|
log.UserString("url", url),
|
|
log.UserString("endpoint", endpoint),
|
|
)
|
|
|
|
if s.tracingEnabled {
|
|
handler = api.TraceHandler(handler, url, s.tracer)
|
|
}
|
|
|
|
// TODO: Add metrics wrapper when available
|
|
// handler = s.metric.wrapHandler(base, handler)
|
|
return s.router.AddRouter(url, endpoint, handler)
|
|
}
|
|
|
|
// StateGetter interface for getting chain state
|
|
type StateGetter interface {
|
|
Get() vm.State
|
|
}
|
|
|
|
// contextKey type for context values
|
|
type contextKey string
|
|
|
|
const stateHolderKey contextKey = "stateHolder"
|
|
|
|
// Reject middleware wraps a handler. If the chain that the context describes is
|
|
// not done state-syncing/bootstrapping, writes back an error.
|
|
func rejectMiddleware(handler http.Handler, ctx *runtime.Runtime) http.Handler {
|
|
// TODO: Add state tracking to consensus context to properly check if chain is bootstrapped
|
|
// For now, allow all requests
|
|
return handler
|
|
}
|
|
|
|
func (s *server) AddAliases(endpoint string, aliases ...string) error {
|
|
url := fmt.Sprintf("%s/%s", baseURL, endpoint)
|
|
endpoints := make([]string, len(aliases))
|
|
for i, alias := range aliases {
|
|
endpoints[i] = fmt.Sprintf("%s/%s", baseURL, alias)
|
|
}
|
|
return s.router.AddAlias(url, endpoints...)
|
|
}
|
|
|
|
func (s *server) AddAliasesWithReadLock(endpoint string, aliases ...string) error {
|
|
// This is safe, as the read lock doesn't actually need to be held once the
|
|
// http handler is called. However, it is unlocked later, so this function
|
|
// must end with the lock held.
|
|
s.router.lock.RUnlock()
|
|
defer s.router.lock.RLock()
|
|
|
|
return s.AddAliases(endpoint, aliases...)
|
|
}
|
|
|
|
func (s *server) Shutdown() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout)
|
|
err := s.srv.Shutdown(ctx)
|
|
cancel()
|
|
|
|
// If shutdown times out, make sure the server is still shutdown.
|
|
_ = s.srv.Close()
|
|
return err
|
|
}
|
|
|
|
type readPathAdder struct {
|
|
pather PathAdderWithReadLock
|
|
}
|
|
|
|
func PathWriterFromWithReadLock(pather PathAdderWithReadLock) PathAdder {
|
|
return readPathAdder{
|
|
pather: pather,
|
|
}
|
|
}
|
|
|
|
func (a readPathAdder) AddRoute(handler http.Handler, base, endpoint string) error {
|
|
return a.pather.AddRouteWithReadLock(handler, base, endpoint)
|
|
}
|
|
|
|
func (a readPathAdder) AddAliases(endpoint string, aliases ...string) error {
|
|
return a.pather.AddAliasesWithReadLock(endpoint, aliases...)
|
|
}
|
|
|
|
func wrapHandler(
|
|
handler http.Handler,
|
|
nodeID ids.NodeID,
|
|
allowedOrigins []string,
|
|
allowedHosts []string,
|
|
) http.Handler {
|
|
h := filterInvalidHosts(handler, allowedHosts)
|
|
h = cors.New(cors.Options{
|
|
AllowedOrigins: allowedOrigins,
|
|
AllowCredentials: true,
|
|
}).Handler(h)
|
|
return http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
// Attach this node's ID as a header
|
|
w.Header().Set("node-id", nodeID.String())
|
|
h.ServeHTTP(w, r)
|
|
},
|
|
)
|
|
}
|