Commit 7d4976d4 authored by Janoš Guljaš's avatar Janoš Guljaš Committed by GitHub

debug api basic and full routing (#1358)

parent 238764b2
...@@ -10,6 +10,7 @@ package debugapi ...@@ -10,6 +10,7 @@ package debugapi
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"net/http" "net/http"
"sync"
"unicode/utf8" "unicode/utf8"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -30,33 +31,32 @@ import ( ...@@ -30,33 +31,32 @@ import (
type Service interface { type Service interface {
http.Handler http.Handler
Configure(p2p p2p.DebugService, pingpong pingpong.Interface, topologyDriver topology.Driver, storer storage.Storer, tags *tags.Tags, accounting accounting.Interface, settlement settlement.Interface, chequebookEnabled bool, swap swap.ApiInterface, chequebook chequebook.Service)
MustRegisterMetrics(cs ...prometheus.Collector) MustRegisterMetrics(cs ...prometheus.Collector)
} }
type server struct { type server struct {
Overlay swarm.Address Overlay swarm.Address
PublicKey ecdsa.PublicKey PublicKey ecdsa.PublicKey
PSSPublicKey ecdsa.PublicKey PSSPublicKey ecdsa.PublicKey
EthereumAddress common.Address EthereumAddress common.Address
P2P p2p.DebugService P2P p2p.DebugService
Pingpong pingpong.Interface Pingpong pingpong.Interface
TopologyDriver topology.Driver TopologyDriver topology.Driver
Storer storage.Storer Storer storage.Storer
Logger logging.Logger Logger logging.Logger
Tracer *tracing.Tracer Tracer *tracing.Tracer
Tags *tags.Tags Tags *tags.Tags
Accounting accounting.Interface Accounting accounting.Interface
Settlement settlement.Interface Settlement settlement.Interface
ChequebookEnabled bool ChequebookEnabled bool
Chequebook chequebook.Service Chequebook chequebook.Service
Swap swap.ApiInterface Swap swap.ApiInterface
metricsRegistry *prometheus.Registry
Options
http.Handler
}
type Options struct {
CORSAllowedOrigins []string CORSAllowedOrigins []string
metricsRegistry *prometheus.Registry
// handler is changed in the Configure method
handler http.Handler
handlerMu sync.RWMutex
} }
// checkOrigin returns true if the origin is not set or is equal to the request host. // checkOrigin returns true if the origin is not set or is equal to the request host.
...@@ -103,28 +103,49 @@ func equalASCIIFold(s, t string) bool { ...@@ -103,28 +103,49 @@ func equalASCIIFold(s, t string) bool {
return s == t return s == t
} }
func New(overlay swarm.Address, publicKey, pssPublicKey ecdsa.PublicKey, ethereumAddress common.Address, p2p p2p.DebugService, pingpong pingpong.Interface, topologyDriver topology.Driver, storer storage.Storer, logger logging.Logger, tracer *tracing.Tracer, tags *tags.Tags, accounting accounting.Interface, settlement settlement.Interface, chequebookEnabled bool, swap swap.ApiInterface, chequebook chequebook.Service, o Options) Service { // New creates a new Debug API Service with only basic routers enabled in order
s := &server{ // to expose /addresses, /health endpoints, Go metrics and pprof. It is useful to expose
Overlay: overlay, // these endpoints before all dependencies are configured and injected to have
PublicKey: publicKey, // access to basic debugging tools and /health endpoint.
PSSPublicKey: pssPublicKey, func New(overlay swarm.Address, publicKey, pssPublicKey ecdsa.PublicKey, ethereumAddress common.Address, logger logging.Logger, tracer *tracing.Tracer, corsAllowedOrigins []string) Service {
EthereumAddress: ethereumAddress, s := new(server)
P2P: p2p, s.Overlay = overlay
Pingpong: pingpong, s.PublicKey = publicKey
TopologyDriver: topologyDriver, s.PSSPublicKey = pssPublicKey
Storer: storer, s.EthereumAddress = ethereumAddress
Logger: logger, s.Logger = logger
Tracer: tracer, s.Tracer = tracer
Tags: tags, s.CORSAllowedOrigins = corsAllowedOrigins
Accounting: accounting, s.metricsRegistry = newMetricsRegistry()
Settlement: settlement,
metricsRegistry: newMetricsRegistry(),
ChequebookEnabled: chequebookEnabled,
Chequebook: chequebook,
Swap: swap,
}
s.setupRouting() s.setRouter(s.newBasicRouter())
return s return s
} }
// Configure injects required dependencies and configuration parameters and
// constructs HTTP routes that depend on them. It is intended and safe to call
// this method only once.
func (s *server) Configure(p2p p2p.DebugService, pingpong pingpong.Interface, topologyDriver topology.Driver, storer storage.Storer, tags *tags.Tags, accounting accounting.Interface, settlement settlement.Interface, chequebookEnabled bool, swap swap.ApiInterface, chequebook chequebook.Service) {
s.P2P = p2p
s.Pingpong = pingpong
s.TopologyDriver = topologyDriver
s.Storer = storer
s.Tags = tags
s.Accounting = accounting
s.Settlement = settlement
s.ChequebookEnabled = chequebookEnabled
s.Chequebook = chequebook
s.Swap = swap
s.setRouter(s.newRouter())
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// protect handler as it is changed by the Configure method
s.handlerMu.RLock()
h := s.handler
s.handlerMu.RUnlock()
h.ServeHTTP(w, r)
}
...@@ -6,6 +6,7 @@ package debugapi_test ...@@ -6,6 +6,7 @@ package debugapi_test
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"encoding/hex"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
...@@ -13,9 +14,14 @@ import ( ...@@ -13,9 +14,14 @@ import (
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee"
accountingmock "github.com/ethersphere/bee/pkg/accounting/mock" accountingmock "github.com/ethersphere/bee/pkg/accounting/mock"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/debugapi" "github.com/ethersphere/bee/pkg/debugapi"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/pkg/logging" "github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/p2p/mock"
p2pmock "github.com/ethersphere/bee/pkg/p2p/mock" p2pmock "github.com/ethersphere/bee/pkg/p2p/mock"
"github.com/ethersphere/bee/pkg/pingpong" "github.com/ethersphere/bee/pkg/pingpong"
"github.com/ethersphere/bee/pkg/resolver" "github.com/ethersphere/bee/pkg/resolver"
...@@ -57,7 +63,8 @@ func newTestServer(t *testing.T, o testServerOptions) *testServer { ...@@ -57,7 +63,8 @@ func newTestServer(t *testing.T, o testServerOptions) *testServer {
settlement := swapmock.New(o.SettlementOpts...) settlement := swapmock.New(o.SettlementOpts...)
chequebook := chequebookmock.NewChequebook(o.ChequebookOpts...) chequebook := chequebookmock.NewChequebook(o.ChequebookOpts...)
swapserv := swapmock.NewApiInterface(o.SwapOpts...) swapserv := swapmock.NewApiInterface(o.SwapOpts...)
s := debugapi.New(o.Overlay, o.PublicKey, o.PSSPublicKey, o.EthereumAddress, o.P2P, o.Pingpong, topologyDriver, o.Storer, logging.New(ioutil.Discard, 0), nil, o.Tags, acc, settlement, true, swapserv, chequebook, debugapi.Options{}) s := debugapi.New(o.Overlay, o.PublicKey, o.PSSPublicKey, o.EthereumAddress, logging.New(ioutil.Discard, 0), nil, nil)
s.Configure(o.P2P, o.Pingpong, topologyDriver, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook)
ts := httptest.NewServer(s) ts := httptest.NewServer(s)
t.Cleanup(ts.Close) t.Cleanup(ts.Close)
...@@ -86,3 +93,113 @@ func mustMultiaddr(t *testing.T, s string) multiaddr.Multiaddr { ...@@ -86,3 +93,113 @@ func mustMultiaddr(t *testing.T, s string) multiaddr.Multiaddr {
} }
return a return a
} }
// TestServer_Configure validates that http routes are correct when server is
// constructed with only basic routes and after it is configured with
// dependencies.
func TestServer_Configure(t *testing.T) {
privateKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
pssPrivateKey, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
overlay := swarm.MustParseHexAddress("ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59c")
addresses := []multiaddr.Multiaddr{
mustMultiaddr(t, "/ip4/127.0.0.1/tcp/7071/p2p/16Uiu2HAmTBuJT9LvNmBiQiNoTsxE5mtNy6YG3paw79m94CRa9sRb"),
mustMultiaddr(t, "/ip4/192.168.0.101/tcp/7071/p2p/16Uiu2HAmTBuJT9LvNmBiQiNoTsxE5mtNy6YG3paw79m94CRa9sRb"),
mustMultiaddr(t, "/ip4/127.0.0.1/udp/7071/quic/p2p/16Uiu2HAmTBuJT9LvNmBiQiNoTsxE5mtNy6YG3paw79m94CRa9sRb"),
}
ethereumAddress := common.HexToAddress("abcd")
o := testServerOptions{
PublicKey: privateKey.PublicKey,
PSSPublicKey: pssPrivateKey.PublicKey,
Overlay: overlay,
EthereumAddress: ethereumAddress,
P2P: mock.New(mock.WithAddressesFunc(func() ([]multiaddr.Multiaddr, error) {
return addresses, nil
})),
}
topologyDriver := topologymock.NewTopologyDriver(o.TopologyOpts...)
acc := accountingmock.NewAccounting(o.AccountingOpts...)
settlement := swapmock.New(o.SettlementOpts...)
chequebook := chequebookmock.NewChequebook(o.ChequebookOpts...)
swapserv := swapmock.NewApiInterface(o.SwapOpts...)
s := debugapi.New(o.Overlay, o.PublicKey, o.PSSPublicKey, o.EthereumAddress, logging.New(ioutil.Discard, 0), nil, nil)
ts := httptest.NewServer(s)
t.Cleanup(ts.Close)
client := &http.Client{
Transport: web.RoundTripperFunc(func(r *http.Request) (*http.Response, error) {
u, err := url.Parse(ts.URL + r.URL.String())
if err != nil {
return nil, err
}
r.URL = u
return ts.Client().Transport.RoundTrip(r)
}),
}
testBasicRouter(t, client)
jsonhttptest.Request(t, client, http.MethodGet, "/readiness", http.StatusNotFound,
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
Message: http.StatusText(http.StatusNotFound),
Code: http.StatusNotFound,
}),
)
jsonhttptest.Request(t, client, http.MethodGet, "/addresses", http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.AddressesResponse{
Overlay: o.Overlay,
Underlay: make([]multiaddr.Multiaddr, 0),
Ethereum: o.EthereumAddress,
PublicKey: hex.EncodeToString(crypto.EncodeSecp256k1PublicKey(&o.PublicKey)),
PSSPublicKey: hex.EncodeToString(crypto.EncodeSecp256k1PublicKey(&o.PSSPublicKey)),
}),
)
s.Configure(o.P2P, o.Pingpong, topologyDriver, o.Storer, o.Tags, acc, settlement, true, swapserv, chequebook)
testBasicRouter(t, client)
jsonhttptest.Request(t, client, http.MethodGet, "/readiness", http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.StatusResponse{
Status: "ok",
Version: bee.Version,
}),
)
jsonhttptest.Request(t, client, http.MethodGet, "/addresses", http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.AddressesResponse{
Overlay: o.Overlay,
Underlay: addresses,
Ethereum: o.EthereumAddress,
PublicKey: hex.EncodeToString(crypto.EncodeSecp256k1PublicKey(&o.PublicKey)),
PSSPublicKey: hex.EncodeToString(crypto.EncodeSecp256k1PublicKey(&o.PSSPublicKey)),
}),
)
}
func testBasicRouter(t *testing.T, client *http.Client) {
t.Helper()
jsonhttptest.Request(t, client, http.MethodGet, "/health", http.StatusOK,
jsonhttptest.WithExpectedJSONResponse(debugapi.StatusResponse{
Status: "ok",
Version: bee.Version,
}),
)
for _, path := range []string{
"/metrics",
"/debug/pprof",
"/debug/pprof/cmdline",
"/debug/pprof/profile?seconds=1", // profile for only 1 second to check only the status code
"/debug/pprof/symbol",
"/debug/pprof/trace",
"/debug/vars",
} {
jsonhttptest.Request(t, client, http.MethodGet, path, http.StatusOK)
}
}
...@@ -24,11 +24,18 @@ type addressesResponse struct { ...@@ -24,11 +24,18 @@ type addressesResponse struct {
} }
func (s *server) addressesHandler(w http.ResponseWriter, r *http.Request) { func (s *server) addressesHandler(w http.ResponseWriter, r *http.Request) {
underlay, err := s.P2P.Addresses() // initialize variable to json encode as [] instead null if p2p is nil
if err != nil { underlay := make([]multiaddr.Multiaddr, 0)
s.Logger.Debugf("debug api: p2p addresses: %v", err) // addresses endpoint is exposed before p2p service is configured
jsonhttp.InternalServerError(w, err) // to provide information about other addresses.
return if s.P2P != nil {
u, err := s.P2P.Addresses()
if err != nil {
s.Logger.Debugf("debug api: p2p addresses: %v", err)
jsonhttp.InternalServerError(w, err)
return
}
underlay = u
} }
jsonhttp.OK(w, addressesResponse{ jsonhttp.OK(w, addressesResponse{
Overlay: s.Overlay, Overlay: s.Overlay,
......
...@@ -19,10 +19,17 @@ import ( ...@@ -19,10 +19,17 @@ import (
"github.com/ethersphere/bee/pkg/logging/httpaccess" "github.com/ethersphere/bee/pkg/logging/httpaccess"
) )
func (s *server) setupRouting() { // newBasicRouter constructs only the routes that do not depend on the injected dependencies:
baseRouter := http.NewServeMux() // - /health
// - pprof
// - vars
// - metrics
// - /addresses
func (s *server) newBasicRouter() *mux.Router {
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(jsonhttp.NotFoundHandler)
baseRouter.Handle("/metrics", web.ChainHandlers( router.Path("/metrics").Handler(web.ChainHandlers(
httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages
web.FinalHandler(promhttp.InstrumentMetricHandler( web.FinalHandler(promhttp.InstrumentMetricHandler(
s.metricsRegistry, s.metricsRegistry,
...@@ -30,9 +37,6 @@ func (s *server) setupRouting() { ...@@ -30,9 +37,6 @@ func (s *server) setupRouting() {
)), )),
)) ))
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(jsonhttp.NotFoundHandler)
router.Handle("/debug/pprof", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { router.Handle("/debug/pprof", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := r.URL u := r.URL
u.Path += "/" u.Path += "/"
...@@ -48,20 +52,31 @@ func (s *server) setupRouting() { ...@@ -48,20 +52,31 @@ func (s *server) setupRouting() {
router.Handle("/health", web.ChainHandlers( router.Handle("/health", web.ChainHandlers(
httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages
web.FinalHandlerFunc(s.statusHandler), web.FinalHandlerFunc(statusHandler),
)) ))
router.Handle("/addresses", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.addressesHandler),
})
return router
}
// newRouter construct the complete set of routes after all of the dependencies
// are injected and exposes /readiness endpoint to provide information that
// Debug API is fully active.
func (s *server) newRouter() *mux.Router {
router := s.newBasicRouter()
router.Handle("/readiness", web.ChainHandlers( router.Handle("/readiness", web.ChainHandlers(
httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages httpaccess.SetAccessLogLevelHandler(0), // suppress access log messages
web.FinalHandlerFunc(s.statusHandler), web.FinalHandlerFunc(statusHandler),
)) ))
router.Handle("/pingpong/{peer-id}", jsonhttp.MethodHandler{ router.Handle("/pingpong/{peer-id}", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.pingpongHandler), "POST": http.HandlerFunc(s.pingpongHandler),
}) })
router.Handle("/addresses", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.addressesHandler),
})
router.Handle("/connect/{multi-address:.+}", jsonhttp.MethodHandler{ router.Handle("/connect/{multi-address:.+}", jsonhttp.MethodHandler{
"POST": http.HandlerFunc(s.peerConnectHandler), "POST": http.HandlerFunc(s.peerConnectHandler),
}) })
...@@ -149,7 +164,13 @@ func (s *server) setupRouting() { ...@@ -149,7 +164,13 @@ func (s *server) setupRouting() {
"GET": http.HandlerFunc(s.getTagHandler), "GET": http.HandlerFunc(s.getTagHandler),
}) })
baseRouter.Handle("/", web.ChainHandlers( return router
}
// setRouter sets the base Debug API handler with common middlewares.
func (s *server) setRouter(router http.Handler) {
h := http.NewServeMux()
h.Handle("/", web.ChainHandlers(
httpaccess.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, s.Tracer, "debug api access"), httpaccess.NewHTTPAccessLogHandler(s.Logger, logrus.InfoLevel, s.Tracer, "debug api access"),
handlers.CompressHandler, handlers.CompressHandler,
func(h http.Handler) http.Handler { func(h http.Handler) http.Handler {
...@@ -164,10 +185,12 @@ func (s *server) setupRouting() { ...@@ -164,10 +185,12 @@ func (s *server) setupRouting() {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
}) })
}, },
// todo: add recovery handler
web.NoCacheHeadersHandler, web.NoCacheHeadersHandler,
web.FinalHandler(router), web.FinalHandler(router),
)) ))
s.Handler = baseRouter s.handlerMu.Lock()
defer s.handlerMu.Unlock()
s.handler = h
} }
...@@ -16,7 +16,7 @@ type statusResponse struct { ...@@ -16,7 +16,7 @@ type statusResponse struct {
Version string `json:"version"` Version string `json:"version"`
} }
func (s *server) statusHandler(w http.ResponseWriter, r *http.Request) { func statusHandler(w http.ResponseWriter, r *http.Request) {
jsonhttp.OK(w, statusResponse{ jsonhttp.OK(w, statusResponse{
Status: "ok", Status: "ok",
Version: bee.Version, Version: bee.Version,
......
...@@ -128,6 +128,39 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, ...@@ -128,6 +128,39 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
tracerCloser: tracerCloser, tracerCloser: tracerCloser,
} }
var debugAPIService debugapi.Service
if o.DebugAPIAddr != "" {
overlayEthAddress, err := signer.EthereumAddress()
if err != nil {
return nil, fmt.Errorf("eth address: %w", err)
}
// set up basic debug api endpoints for debugging and /health endpoint
debugAPIService = debugapi.New(swarmAddress, publicKey, pssPrivateKey.PublicKey, overlayEthAddress, logger, tracer, o.CORSAllowedOrigins)
debugAPIListener, err := net.Listen("tcp", o.DebugAPIAddr)
if err != nil {
return nil, fmt.Errorf("debug api listener: %w", err)
}
debugAPIServer := &http.Server{
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 3 * time.Second,
Handler: debugAPIService,
ErrorLog: log.New(b.errorLogWriter, "", 0),
}
go func() {
logger.Infof("debug api address: %s", debugAPIListener.Addr())
if err := debugAPIServer.Serve(debugAPIListener); err != nil && err != http.ErrServerClosed {
logger.Debugf("debug api server: %v", err)
logger.Error("unable to serve debug api")
}
}()
b.debugAPIServer = debugAPIServer
}
stateStore, err := InitStateStore(logger, o.DataDir) stateStore, err := InitStateStore(logger, o.DataDir)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -438,12 +471,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, ...@@ -438,12 +471,7 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
b.apiCloser = apiService b.apiCloser = apiService
} }
if o.DebugAPIAddr != "" { if debugAPIService != nil {
// Debug API server
debugAPIService := debugapi.New(swarmAddress, publicKey, pssPrivateKey.PublicKey, overlayEthAddress, p2ps, pingPong, kad, storer, logger, tracer, tagService, acc, settlement, o.SwapEnable, swapService, chequebookService, debugapi.Options{
CORSAllowedOrigins: o.CORSAllowedOrigins,
})
// register metrics from components // register metrics from components
debugAPIService.MustRegisterMetrics(p2ps.Metrics()...) debugAPIService.MustRegisterMetrics(p2ps.Metrics()...)
debugAPIService.MustRegisterMetrics(pingPong.Metrics()...) debugAPIService.MustRegisterMetrics(pingPong.Metrics()...)
...@@ -470,28 +498,8 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey, ...@@ -470,28 +498,8 @@ func NewBee(addr string, swarmAddress swarm.Address, publicKey ecdsa.PublicKey,
debugAPIService.MustRegisterMetrics(l.Metrics()...) debugAPIService.MustRegisterMetrics(l.Metrics()...)
} }
debugAPIListener, err := net.Listen("tcp", o.DebugAPIAddr) // inject dependencies and configure full debug api http path routes
if err != nil { debugAPIService.Configure(p2ps, pingPong, kad, storer, tagService, acc, settlement, o.SwapEnable, swapService, chequebookService)
return nil, fmt.Errorf("debug api listener: %w", err)
}
debugAPIServer := &http.Server{
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 3 * time.Second,
Handler: debugAPIService,
ErrorLog: log.New(b.errorLogWriter, "", 0),
}
go func() {
logger.Infof("debug api address: %s", debugAPIListener.Addr())
if err := debugAPIServer.Serve(debugAPIListener); err != nil && err != http.ErrServerClosed {
logger.Debugf("debug api server: %v", err)
logger.Error("unable to serve debug api")
}
}()
b.debugAPIServer = debugAPIServer
} }
if err := kad.Start(p2pCtx); err != nil { if err := kad.Start(p2pCtx); err != nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment